DEV Community

Nomad Wozo
Nomad Wozo

Posted on AI-assisted

Why 6% isn't 6%: APY vs. nominal rate in JavaScript (and the double-compounding bug)

I build small finance calculators that run entirely in the browser. The sneakiest bug in this kind of code isn't floating point. It's feeding the wrong kind of rate into the formula.

Here's the short version, then the code.

Two rates, one label

A savings account can quote two numbers:

  • Nominal annual rate (US deposit rules let banks call it the "annual percentage rate"): the yearly rate before compounding.
  • APY (annual percentage yield): what you actually earn in a year once compounding is included.

US Regulation DD defines the interest rate as "the annual rate of interest paid on an account which does not reflect compounding" and the APY as the rate "based on the interest rate and the frequency of compounding for a 365-day period" (12 CFR 1030.2). When a deposit ad states a rate, it has to state it as an APY, and the plain interest rate can't be shown more prominently (12 CFR 1030.8).

So if your calculator compounds monthly, it wants the nominal rate. Users, meanwhile, mostly have the APY, because that's what's on the bank's website.

The two functions you need

// nominal annual rate -> APY, compounded n times per year
const apy = (rate, n) => (1 + rate / n) ** n - 1;

// APY -> nominal annual rate for n compounding periods
const nominalFromApy = (y, n = 12) => n * ((1 + y) ** (1 / n) - 1);

const balance = (principal, rate, years, n = 12) =>
  principal * (1 + rate / n) ** (n * years);
Enter fullscreen mode Exit fullscreen mode

How much does compounding frequency matter?

Same 6% nominal rate, $10,000, 30 years, nothing added:

for (const [name, n] of [["annually", 1], ["quarterly", 4], ["monthly", 12], ["daily", 365]]) {
  console.log(name, apy(0.06, n), balance(10_000, 0.06, 30, n));
}
Enter fullscreen mode Exit fullscreen mode
Compounding APY After 30 years
Annually 6.0000% $57,434.91
Quarterly 6.1364% $59,693.23
Monthly 6.1678% $60,225.75
Daily 6.1831% $60,487.53
Continuous (Math.exp(0.06) - 1\) 6.1837% n/a

Annual to monthly is the big jump. Monthly to daily barely moves the needle, and continuous compounding is the ceiling. If you're agonizing over daily vs. monthly in your code, you can relax a bit.

The double-compounding bug

A user sees "6.00% APY," types 6 into your monthly-compounding calculator, and you happily compound it again:

const advertisedApy = 0.06;

balance(10_000, advertisedApy, 10);                 // 18193.97  (too high)
balance(10_000, nominalFromApy(advertisedApy), 10); // 17908.48  (matches a true 6% APY)
Enter fullscreen mode Exit fullscreen mode

That's $285.49 of imaginary money after 10 years on $10,000. Not huge, but it's the kind of error that makes someone distrust every other number on the page. And it grows with the rate and the time horizon.

nominalFromApy(0.06)\ is about 5.8411%. A few more conversions, monthly compounding:

Advertised APY Nominal rate to enter
3.5% 3.4451%
4.0% 3.9285%
5.0% 4.8889%
6.0% 5.8411%

Things I'd do in any calculator UI

  1. Label the input with the rate type. "Annual interest rate (nominal, compounded monthly)" is ugly but honest. A small "Have an APY? Convert it" helper is nicer.
  2. Be explicit about rounding. If your rate field steps by 0.1, a 6% APY becomes 5.8% (APY 5.957%), not 5.8411%. Close enough for planning, but say so.
  3. Round only for display. Keep full precision through the loop and round to cents at the end:
   const roundCents = (x) => Math.round((x + Number.EPSILON) * 100) / 100;
   roundCents(balance(10_000, 0.06, 1)); // 10616.78
Enter fullscreen mode Exit fullscreen mode

(And yes, 0.1 + 0.2\ is still 0.30000000000000004\. For a planning calculator, floats plus display rounding are fine. For anything that moves real money, use integer cents or a decimal library.)

  1. Show the formula. Users catch rate-type mistakes much faster when they can see (1 + r/12)^12 − 1\ on the page.
  2. Don't call a loan APR an APY cousin. Loan APR follows separate disclosure rules and can bake in fees. Everything above is about deposit interest only.

Why I keep this math client-side

None of this needs a server. It's a few exponentials, so the whole calculation can run in the browser and the user's numbers never leave their device. For financial inputs, that feels like the right default, and it makes the app work offline too.


Disclosure: I'm the maker of FINORA, a set of free browser-based financial calculators (no sign-up). Its compound interest calculator takes a nominal rate and compounds monthly, which is where the 0.1-step note above comes from: https://finance.wooooju.com/en/calculator/compound-interest/ . No affiliate links in this post. This is math, not financial advice.

Top comments (0)