Dev.to WebDev 🛠 Dev 👁 0 📖 2 min read

The VAT Bug I Almost Shipped: Why Gross (1 + Rate) Is Not Optional

The VAT Bug I Almost Shipped: Why Gross ÷ (1 + Rate) Is Not Optional Building an invoicing feature a while back, I wrote this and moved on: const vatAmount = grossPrice * (vatRate / 100); const netPrice = grossPrice -

The VAT Bug I Almost Shipped: Why Gross ÷ (1 + Rate) Is Not Optional

Building an invoicing feature a while back, I wrote this and moved on:

const vatAmount = grossPrice * (vatRate / 100);
const netPrice = grossPrice - vatAmount;

Looked fine. Passed my one manual test. Shipped. Then a user reported their invoice numbers did not match their accountant's figures, and I spent an hour convinced the bug was somewhere else before I actually checked the math by hand.

The bug is that function. It treats the gross price as 100% and subtracts a flat percentage off the top. But if VAT was already added to get the gross figure, the gross price is already 120% of the net, not 100%. Subtracting a straight 20% from it does not reverse the calculation, it just produces a different wrong number that happens to look plausible.

The Actual Formula

Reversing VAT requires division, not subtraction:

function removeVat(grossPrice, vatRatePercent) {
const rate = vatRatePercent / 100;
const netPrice = grossPrice / (1 + rate);
const vatAmount = grossPrice - netPrice;
return { netPrice, vatAmount };
}

removeVat(120, 20);
// { netPrice: 100, vatAmount: 20 }

Run the broken version on the same input and you get a net price of 96, off by 4. Small numbers hide the bug. Larger invoices make it obvious fast, and by then it has usually already gone out to a client.

Where This Actually Bites in Production

If you are building anything that touches invoicing, expense tracking, or e-commerce checkout across regions, this shows up constantly because the divisor changes per country and you cannot hardcode 1.20 everywhere:

const vatRates = {
UK: 20,
DE: 19,
NL: 21,
CH: 8.1, // outside the EU VAT system entirely
};

function removeVatByCountry(grossPrice, countryCode) {
const rate = vatRates[countryCode];
return grossPrice / (1 + rate / 100);
}

Switzerland is the one that catches people off guard in code reviews, it is not in the EU VAT system at all, so if your rates object assumes EU membership as a data source, Switzerland needs to be handled as a separate case rather than inherited from a shared EU rate table.

Floating Point Gets You Next

Once the formula is right, the next bug is rounding. Division introduces repeating decimals fast, and if you round at the wrong step, the net price and VAT amount stop summing back to the original gross price, which is exactly the kind of discrepancy an accountant will catch immediately.

function removeVatSafe(grossPriceCents, vatRatePercent) {
const rate = vatRatePercent / 100;
const netPriceCents = Math.round(grossPriceCents / (1 + rate));
const vatAmountCents = grossPriceCents - netPriceCents;
return { netPriceCents, vatAmountCents };
}

Working in the smallest currency unit, cents instead of decimal pounds, and rounding once at the end, keeps the two numbers reconciling correctly instead of drifting by a penny on large batches of invoices.

I Wrote This Up Properly, With the Full Rate Table

Put together a longer breakdown covering the UK, Germany, Netherlands, and Switzerland, the specific 2026 rate changes that affect this year's numbers, plus worked examples. Full writeup is here (https://utilvance.com/blog/post.php?slug=reverse-vat-calculator-2026) if you want the reference rates without digging through gov sites for each country.

There is also a free VAT Calculator (https://utilvance.com/tools/vat-calculator.php) with a Remove VAT mode if you just need the number fast without wiring up the function yourself.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.