Skip to content
StepUp Law logo StepUp Law

I Audited My Own Tax Calculator and Found Two Bugs

Published 2026-08-19

I put a Roth conversion calculator online. The next day I sat down and checked it against the IRS documents line by line, and I found two bugs. Both were in the same deduction. Both made a married couple’s tax bill look smaller than it is.

My test suite passed the whole time. I want to write down why, because the reason has nothing to do with the code being right.

What the thing does

A Roth conversion gets taxed as ordinary income. Most calculators multiply what you convert by your tax bracket and stop. For a retiree that’s only the first step, because four other things move at the same time.

More of your Social Security becomes taxable. Your dividends and long-term gains can jump from the 0% rate to 15%. There’s a new deduction for people over 65 that shrinks as your income goes up. And your Medicare premium two years from now gets set by this year’s income, at thresholds where one dollar over costs you the whole surcharge.

Put the first two together and someone in the 12% bracket can pay 49.95% on the next dollar they convert. That’s what I built the thing for.

Bug one

The 2025 tax law gives people 65 and over a $6,000 deduction. It shrinks by 6% of whatever your income is above $75,000, or $150,000 if you’re married. I read that and wrote the obvious line.

// mine
deduction = max(0, people * 6000 - 0.06 * excessIncome)

The statute shrinks the $6,000 each person gets, and then each spouse claims their shrunken amount. So the multiplication moves.

// the statute
deduction = people * max(0, 6000 - 0.06 * excessIncome)

For one person those are the same number. Hold onto that, it matters in a minute.

For a couple where both are 65 they’re very different. At $250,000 of income mine still handed them $6,000. The real answer is zero. Mine kept the deduction alive until $350,000 when it actually dies at $250,000.

It also got the marginal rate wrong, which is embarrassing, because showing you the real marginal rate is the whole reason the calculator exists. A couple in that range loses 12 cents of deduction per dollar they earn, not 6.

Bug two

That deduction phases out based on “modified adjusted gross income.” I had one MAGI variable and I used it everywhere.

There are three of them and they’re different. Medicare counts your tax-exempt interest. The Social Security formula counts it too. This deduction doesn’t, and the IRS form has no line for it. So anyone holding municipal bonds saw too small a deduction and too big a tax bill.

Why my tests missed both

I had 40-odd tests. Hand-computed, every bracket boundary covered. Two things were wrong with them.

I wrote the expected answers from the same wrong idea that produced the code. My test said the deduction at $200,000 for a couple was $9,000, and it passed, because I got $9,000 out of my head and I’d put the same wrong thing in the code an hour earlier. The test wasn’t comparing my code to the law. It was comparing my code to my memory, and both were wrong the same way. If you write the test and the code from one understanding, the test can’t tell you the understanding is broken.

Almost all my cases were single filers. Look at those two formulas again. When there’s one person they’re identical. So most of my suite physically could not tell the right version from the wrong one.

There’s a third thing, and it’s the one I’d most want another developer to hear.

Comparing your code to a copy of your code proves nothing

The calculator ships twice. There’s a TypeScript library, and there’s a single-file browser widget other sites can embed, which carries its own copy of the tax tables because it has to run with no dependencies.

Two implementations, so I wrote the check you’d expect. Make 40,000 random taxpayers, run both, make sure every answer matches to the penny.

It passed. It was always going to pass. Both copies had the same bug, because I built the second one by porting the first. That test told me I’d copied carefully. I’d been reading the green check as though it told me the math was right.

If the two implementations come from different people or different sources, comparing them is strong evidence. If one is your own port of the other, you’re testing your typing.

What actually would have caught it

The IRS publishes a form for this deduction. Schedule 1-A, Part V. Line 35 works out one shrunken amount. Line 36a puts that amount down for you. Line 36b puts the same amount down for your spouse. Line 37 adds them up.

You can see the bug just reading the line numbers. It shrinks once, then gets claimed twice.

So I stopped writing my own expected answers. Now the test is the form.

// the answer comes from the IRS form, not from me
function schedule1A_PartV(magi, filingJointly, taxpayer65, spouse65) {
  const threshold = filingJointly ? 150000 : 75000;
  const line33 = Math.max(0, magi - threshold);
  const line34 = line33 * 0.06;
  const line35 = Math.max(0, 6000 - line34);
  const line36a = taxpayer65 ? line35 : 0;
  const line36b = (filingJointly && spouse65) ? line35 : 0;
  return line36a + line36b;                       // line 37
}

Then I ran my code against it across 24,012 combinations of filing status, ages and income levels. Zero mismatches. None of my opinions are anywhere in that check, which is why I trust it.

I did something similar for the tax brackets, using arithmetic instead of a form. In any bracket table, the base tax on each row has to equal the row above it plus that row’s rate times its width. That’s true of every table the IRS publishes, so one mistyped digit breaks it. All four 2026 tables pass. That’s a much better statement than “I checked the numbers twice.”

And where planners have published a figure, I pinned my engine to it. The Social Security phase-in produces 22.2% and 40.7% marginal rates that show up in the literature, and 49.95% once capital gains get pushed up a bracket. Those are tests now. If I change something and one of those numbers moves, I broke it.

Three things I’d tell another developer

Don’t write your own answer key. If you compute the expected value by hand, you’ve checked your understanding twice and your code zero times. Go find something outside your head. In tax software the agency hands you worksheets that read like pseudocode. In other work it might be a reference implementation, a published table, or a property that has to hold.

Ask whether your test cases could even fail. My single-filer tests weren’t weak. They were incapable. The right and wrong formulas give the same answer when the count is one. If a test would pass whether or not the bug exists, it isn’t covering that behavior.

Know what you’re proving when you compare two implementations. Mine were both wrong in exactly the same way and the comparison came back clean.

One more thing

Everything’s fixed, the corrections are live, and the page shows the date I verified each figure. The bugs were up for about a day.

I could have patched it quietly. But the pitch for this calculator is that you can check my work, and that only means something if I tell you what happened when somebody did. This time it was me, on the second pass.

The calculator is at stepuplaw.com/roth-conversion-calculator. It’s free, and nothing you type leaves your browser. The library and the tests are on GitHub.



Reviewed by Kevin D. Klagge, Esq., Fla. Bar No. 99502. Attorney Kevin Klagge represents families, businesses, and international clients in estate and tax planning, business structuring, and international law, with a focus on Florida legal tools. He litigates estate and business issues in court. This article is general information about Florida law, not legal advice, and does not create an attorney-client relationship.

Questions about your situation?

Book a free 30-minute consult. Flat fees, plain answers, no pressure.

Book your free consult