DEV Community

BANIYA MANI TEJA
BANIYA MANI TEJA

Posted on

I Built a DSCR Calculator in Vanilla JavaScript — Here's the Loan Math Nobody Explains

A while back I needed a DSCR calculator that wasn't buried under popups and didn't ask me to create an account before showing a single number. So I built one: a free, no-signup debt service coverage ratio calculator for rental property investors. It runs entirely in the browser.

It sounded like a weekend project. The math had opinions.

Here's what I learned, with the actual logic.

What DSCR actually is

DSCR (debt service coverage ratio) answers one question: for every dollar of loan payment a rental property owes, how many dollars of operating income does it produce?

DSCR = Net Operating Income / Annual Debt Service
Enter fullscreen mode Exit fullscreen mode
  • Net operating income (NOI) = annual rent minus operating expenses (taxes, insurance, maintenance, management, vacancy). Not minus the mortgage — the mortgage is what we're measuring against.
  • Annual debt service = the yearly loan payment, principal + interest.

A DSCR of 1.25 means the property produces $1.25 for every $1.00 of debt payment. Most investment-property lenders want to see 1.25x. Below 1.00, the property can't cover its own loan.

The amortization formula (the part everyone gets wrong)

The monthly payment on a fixed-rate loan comes from the standard amortization formula:

M = P * r(1+r)^n / ((1+r)^n - 1)
Enter fullscreen mode Exit fullscreen mode

Where P is principal, r is the monthly rate, n is the number of payments. In code:

function monthlyPayment(principal, annualRatePct, years) {
  const r = annualRatePct / 100 / 12;
  const n = years * 12;
  if (r === 0) return principal / n; // edge case: 0% loans
  const factor = Math.pow(1 + r, n);
  return (principal * r * factor) / (factor - 1);
}
Enter fullscreen mode Exit fullscreen mode

That r === 0 guard matters. Without it, a 0% rate gives you 0/0 = NaN, and your calculator silently shows nothing. Real users do type 0 — usually to model an all-cash scenario or a family loan.

Floating point money is a lie (so round deliberately)

JavaScript's floating point will happily tell you a monthly payment is 4055.459999999999. For money, I round only at display time and keep full precision internally:

const annualDebtService = monthlyPayment(580000, 7.5, 30) * 12;
// => 48665.51...
const dscr = 62400 / annualDebtService;
// => 1.2824...
Enter fullscreen mode Exit fullscreen mode

Display 1.28, compute with 1.2824. Rounding intermediate steps is how you get calculators that disagree with each other by a cent and erode trust.

The full calculation

function calculateDSCR({ annualRent, operatingExpenses, loanAmount, annualRatePct, years }) {
  const noi = annualRent - operatingExpenses;
  const annualDebtService = monthlyPayment(loanAmount, annualRatePct, years) * 12;
  return {
    noi,
    annualDebtService,
    dscr: noi / annualDebtService,
  };
}

// $96k rent, $33.6k expenses, $580k at 7.5% for 30 years
calculateDSCR({ annualRent: 96000, operatingExpenses: 33600, loanAmount: 580000, annualRatePct: 7.5, years: 30 });
// => { noi: 62400, annualDebtService: 48665.51, dscr: 1.2824 }
Enter fullscreen mode Exit fullscreen mode

The domain trap: two methods, one name

The trickiest bug wasn't in the code — it was in the domain. There are two different DSCR methods, and they give different answers on the same property:

  1. NOI method: subtract operating expenses (including taxes and insurance) from income first, divide by principal + interest.
  2. Rent-over-PITIA method: divide gross rent by PITIA (principal, interest, taxes, insurance, association dues). Nothing subtracted from income.

Same property, same loan, different ratios. I made the calculator use the NOI method and explain both, because investors who don't know the difference discover it mid-underwriting — when the deal is already under contract.

Working backwards: the feature users actually want

The most useful thing the calculator does isn't computing DSCR from inputs — it's inverting the formula. Given a target DSCR (say 1.25), what's the maximum loan the property supports?

function maxLoan(noi, targetDSCR, annualRatePct, years) {
  const maxAnnualPayment = noi / targetDSCR;
  const r = annualRatePct / 100 / 12;
  const n = years * 12;
  const factor = Math.pow(1 + r, n);
  // invert the amortization formula to solve for P
  return (maxAnnualPayment / 12) * (factor - 1) / (r * factor);
}

maxLoan(62400, 1.25, 7.5, 30); // => ~$594,953
Enter fullscreen mode Exit fullscreen mode

That number is the investor's negotiation ceiling. If the seller won't meet it, the math already said no.

Lessons

  1. Guard your edge cases — 0% rates, zero expenses, and empty inputs all produce NaN if you let them.
  2. Round at display, never in the pipeline — intermediate rounding is how calculators disagree.
  3. The domain is harder than the code — the two-method trap took more research than the entire amortization implementation.
  4. Show your math — every existing calculator I found hid its formula. Showing a worked example is what makes users trust the output.

The whole thing is a single HTML file with vanilla JavaScript — no framework, no build step, no backend. Financial data never leaves the browser, which is the entire point.

Try it live: DSCR Calculator — free, no signup. And if you spot a math edge case I missed, I'd genuinely like to hear about it in the comments.

Top comments (0)