Try it live: run the numbers yourself with the free loan calculator on FreeKalc — no sign-up, no backend, just the maths in your browser.
Ask a developer to model a loan and a mortgage and you'll get two functions that look suspiciously alike. That's because, under the hood, they are the same calculation. A mortgage is just a loan with a house attached — but the differences that matter to borrowers (collateral, term length, escrow, insurance) change the code in ways that are easy to get wrong. Let's build both, side by side, and see exactly where they diverge.
What amortisation actually means
Amortisation is the process of paying off a debt in fixed instalments, where each payment is split between interest and principal. Early on, most of your payment is interest. Over time the balance shrinks, the interest portion falls, and more of each payment chips away at the principal.
Every amortising loan — personal loan, car loan, mortgage — runs on one formula, the standard annuity payment:
M = P × r(1 + r)^n / ((1 + r)^n − 1)
Where P is the principal, r is the monthly interest rate, and n is the total number of payments. If you can implement this once, you can price almost any fixed-rate loan on the planet.
The one function that powers both
function monthlyPayment(principal, annualRate, years) {
const r = annualRate / 100 / 12; // monthly rate as a decimal
const n = years * 12; // total number of payments
if (r === 0) return principal / n; // interest-free edge case
return (principal * r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);
}
// A £25,000 personal loan at 7% over 5 years
console.log(monthlyPayment(25000, 7, 5).toFixed(2)); // 495.03
// A £300,000 mortgage at 4.5% over 30 years
console.log(monthlyPayment(300000, 4.5, 30).toFixed(2)); // 1520.06
Same function, different inputs. The loan is shorter and usually pricier; the mortgage is longer and usually cheaper. But the maths doesn't care what the money is for.
Building the schedule: where the split appears
A payment amount is only half the story. Borrowers want to see where each payment goes. Here's a schedule builder that works for both:
function buildSchedule(principal, annualRate, years, extraPayment = 0) {
const r = annualRate / 100 / 12;
const basePayment = monthlyPayment(principal, annualRate, years);
const schedule = [];
let balance = principal;
let month = 0;
while (balance > 0.01 && month < years * 12 * 2) {
month += 1;
const interest = balance * r;
let principalPaid = basePayment - interest + extraPayment;
if (principalPaid > balance) principalPaid = balance;
balance -= principalPaid;
schedule.push({
month,
payment: +(interest + principalPaid).toFixed(2),
interest: +interest.toFixed(2),
principal: +principalPaid.toFixed(2),
balance: +Math.max(balance, 0).toFixed(2),
});
}
return schedule;
}
const loanSchedule = buildSchedule(25000, 7, 5);
console.log(loanSchedule[0]);
// { month: 1, payment: 495.03, interest: 145.83, principal: 349.20, balance: 24650.80 }
Run that for a 30-year mortgage and the first row tells the whole story: on a £300,000 loan at 4.5%, the first payment of £1,520.06 contains £1,125.00 of interest and only £395.06 of principal. That's the amortisation curve in one line — and it's why the first five years of a mortgage feel like treading water.
Loan vs mortgage: the real differences
| Personal loan | Mortgage | |
|---|---|---|
| Collateral | Usually none (unsecured) | The property itself |
| Typical term | 1–7 years | 15–30 years |
| Rate | Higher (risk priced in) | Lower (secured) |
| Extra costs | Origination fee, maybe | Escrow, PMI, taxes, insurance |
| What happens on default | Collections, credit damage | Foreclosure — you lose the home |
The first three rows are just parameters to the same function. The last two rows are where your code has to change.
1. Escrow and PMI belong in the mortgage version only
A mortgage payment quoted to a borrower is rarely just principal and interest. Taxes and homeowner's insurance are usually collected monthly into escrow, and if the deposit is under 20%, private mortgage insurance (PMI) is added too. A loan calculator that ignores these understates the real monthly cost by hundreds.
function mortgagePayment({ principal, annualRate, years, annualTax, annualInsurance, pmiMonthly = 0 }) {
const pi = monthlyPayment(principal, annualRate, years);
const escrow = (annualTax + annualInsurance) / 12;
return {
principalAndInterest: +pi.toFixed(2),
escrow: +escrow.toFixed(2),
pmi: pmiMonthly,
total: +(pi + escrow + pmiMonthly).toFixed(2),
};
}
console.log(mortgagePayment({
principal: 300000, annualRate: 4.5, years: 30,
annualTax: 3600, annualInsurance: 1800, pmiMonthly: 125,
}));
// { principalAndInterest: 1520.06, escrow: 450, pmi: 125, total: 2095.06 }
The "real" payment is £2,095.06, not £1,520.06. That's the number that decides whether someone can afford the house — and it's the number a naive loan formula hides.
2. PMI drops off; escrow doesn't
PMI typically ends once the loan-to-value ratio hits 80%, which your schedule can detect by watching the balance cross that threshold. Escrow, on the other hand, lasts the life of the loan and gets recalculated annually as tax assessments change. Model PMI as a phase, not a constant:
function monthsUntilPmiDrops(principal, homeValue, annualRate, years) {
const schedule = buildSchedule(principal, annualRate, years);
const threshold = homeValue * 0.8;
const row = schedule.find((entry) => entry.balance <= threshold);
return row ? row.month : null; // month PMI can be requested to stop
}
3. Extra payments matter far more on a mortgage
An extra £100 a month on a 5-year loan saves a little interest. The same £100 on a 30-year mortgage can shave years off the term and tens of thousands off total interest, because it attacks the principal when the balance — and therefore the interest — is at its largest. That's why the extraPayment parameter in buildSchedule exists, and why good calculators let users experiment with it.
Edge cases that break naive implementations
-
0% interest: the standard formula divides by zero when
r = 0. Guard it (as above) and returnprincipal / n. - Rounding drift: rounding each month's interest to two decimals accumulates error. Keep full precision internally and round only for display — and always cap the final payment at the remaining balance so the schedule ends at exactly zero, never at −£0.03.
- Balloon payments: some loans end with a large final lump sum. Model this by treating the balloon as a remaining balance at term end rather than forcing the schedule to zero.
- Variable rates: an adjustable-rate mortgage re-prices after the fixed period. The honest way to model it is to rebuild the schedule at each rate change, using the remaining balance as the new principal.
- Fees in the APR: the advertised rate and the APR differ when origination fees exist. If you're comparing loans, compute the payment from the APR, not the headline rate.
The takeaway
A loan and a mortgage share one formula, one schedule builder, and one amortisation curve. What makes a mortgage different isn't the maths — it's the layers wrapped around it: escrow, PMI, a 30-year horizon where small choices compound into huge sums, and a house on the line if the numbers are wrong. Build the shared core once, then add the mortgage-specific layers as separate, testable functions. Your users — and their budgets — will thank you.
Want to see the numbers on your own loan? The free loan calculator on FreeKalc runs this exact maths in your browser — try the extra-payment option and watch years fall off the schedule.
Top comments (0)