<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nomad Wozo</title>
    <description>The latest articles on DEV Community by Nomad Wozo (@wozo).</description>
    <link>https://dev.to/wozo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4180682%2F3c234f3f-1bb1-458e-973d-d8d64d64f436.jpg</url>
      <title>DEV Community: Nomad Wozo</title>
      <link>https://dev.to/wozo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wozo"/>
    <language>en</language>
    <item>
      <title>Why 6% isn't 6%: APY vs. nominal rate in JavaScript (and the double-compounding bug)</title>
      <dc:creator>Nomad Wozo</dc:creator>
      <pubDate>Thu, 22 Oct 2026 02:43:22 +0000</pubDate>
      <link>https://dev.to/wozo/why-6-isnt-6-apy-vs-nominal-rate-in-javascript-and-the-double-compounding-bug-io4</link>
      <guid>https://dev.to/wozo/why-6-isnt-6-apy-vs-nominal-rate-in-javascript-and-the-double-compounding-bug-io4</guid>
      <description>&lt;p&gt;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 &lt;em&gt;kind&lt;/em&gt; of rate into the formula.&lt;/p&gt;

&lt;p&gt;Here's the short version, then the code.&lt;/p&gt;

&lt;h4&gt;
  
  
  Two rates, one label
&lt;/h4&gt;

&lt;p&gt;A savings account can quote two numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nominal annual rate&lt;/strong&gt; (US deposit rules let banks call it the "annual percentage rate"): the yearly rate &lt;em&gt;before&lt;/em&gt; compounding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APY (annual percentage yield)&lt;/strong&gt;: what you actually earn in a year once compounding is included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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" (&lt;a href="https://www.ecfr.gov/current/title-12/chapter-X/part-1030/section-1030.2" rel="noopener noreferrer"&gt;12 CFR 1030.2&lt;/a&gt;). 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 (&lt;a href="https://www.ecfr.gov/current/title-12/chapter-X/part-1030/section-1030.8" rel="noopener noreferrer"&gt;12 CFR 1030.8&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;So if your calculator compounds monthly, it wants the &lt;strong&gt;nominal&lt;/strong&gt; rate. Users, meanwhile, mostly have the &lt;strong&gt;APY&lt;/strong&gt;, because that's what's on the bank's website.&lt;/p&gt;

&lt;h4&gt;
  
  
  The two functions you need
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// nominal annual rate -&amp;gt; APY, compounded n times per year&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// APY -&amp;gt; nominal annual rate for n compounding periods&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nominalFromApy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;principal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  How much does compounding frequency matter?
&lt;/h4&gt;

&lt;p&gt;Same 6% nominal rate, $10,000, 30 years, nothing added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;annually&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quarterly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;monthly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;daily&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;apy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.06&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.06&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Compounding&lt;/th&gt;
&lt;th&gt;APY&lt;/th&gt;
&lt;th&gt;After 30 years&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Annually&lt;/td&gt;
&lt;td&gt;6.0000%&lt;/td&gt;
&lt;td&gt;$57,434.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;6.1364%&lt;/td&gt;
&lt;td&gt;$59,693.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;6.1678%&lt;/td&gt;
&lt;td&gt;$60,225.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;6.1831%&lt;/td&gt;
&lt;td&gt;$60,487.53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous (&lt;code&gt;Math.exp(0.06) - 1\&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;6.1837%&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h4&gt;
  
  
  The double-compounding bug
&lt;/h4&gt;

&lt;p&gt;A user sees "6.00% APY," types 6 into your monthly-compounding calculator, and you happily compound it again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;advertisedApy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.06&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;advertisedApy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                 &lt;span class="c1"&gt;// 18193.97  (too high)&lt;/span&gt;
&lt;span class="nf"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;nominalFromApy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;advertisedApy&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 17908.48  (matches a true 6% APY)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nominalFromApy(0.06)\&lt;/code&gt; is about 5.8411%. A few more conversions, monthly compounding:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Advertised APY&lt;/th&gt;
&lt;th&gt;Nominal rate to enter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3.5%&lt;/td&gt;
&lt;td&gt;3.4451%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.0%&lt;/td&gt;
&lt;td&gt;3.9285%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.0%&lt;/td&gt;
&lt;td&gt;4.8889%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6.0%&lt;/td&gt;
&lt;td&gt;5.8411%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Things I'd do in any calculator UI
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Label the input with the rate type.&lt;/strong&gt; "Annual interest rate (nominal, compounded monthly)" is ugly but honest. A small "Have an APY? Convert it" helper is nicer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be explicit about rounding.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round only for display.&lt;/strong&gt; Keep full precision through the loop and round to cents at the end:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;roundCents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EPSILON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="nf"&gt;roundCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.06&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 10616.78&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;h4&gt;
  
  
  Why I keep this math client-side
&lt;/h4&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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: &lt;a href="https://finance.wooooju.com/en/calculator/compound-interest/" rel="noopener noreferrer"&gt;https://finance.wooooju.com/en/calculator/compound-interest/&lt;/a&gt; . No affiliate links in this post. This is math, not financial advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>math</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
