I found a rounding bug in `Number().toFixed()` in every JavaScript environment I’ve tried (Chrome, Firefox, Internet Explorer, Brave, and Node.js). The fix is surprisingly simple. Read on…

## Warm Up

I found this version of the rounding bug in `toFixed()` while revising a number-formatting function that performs the same kind of thing as `Intl.NumberFormat#format()`.

``````(1.015).toFixed(2) // returns "1.01" instead of "1.02"
``````

The failing test is on line 42 here. I had missed it until December 2017, and that spurred me to check for other problems.

## Bug Reports

There is a long history of bug reports with respect to rounding errors using `toFixed()`.

In general, these point out a bug for a value, but none reports a range or pattern of values returning erroneous results (at least none that I have found, I may have missed something). That leaves the programmers to focus on the without seeing a larger pattern. I don’t blame them for that.

## Finding the Pattern

Unexpected results based on input must arise from a shared pattern in the input. So, rather than review the specification for `Number().toFixed()`, I focused on testing with a series of values to determine where the bug shows up in each series.

### Test Function

I created the following test function to exercise `toFixed()` over a series of integers ranging from 1 to a `maxValue`, adding the `fraction` such as .005 to each integer. The `fixed` (number of digits) argument to `toFixed()` is calculated from the length of the `fraction` value.

``````    function test({fraction, maxValue}) {

// Happy side-effect: `toString()` removes trailing zeroes.
fraction = fraction.toString()
var fixLength = fraction.split('.')[1].length - 1

// All this to create the expectedFraction message...
var last = Number(fraction.charAt(fraction.length - 1))
var fixDigit = Number(fraction.charAt(fraction.length - 2))

last &gt;= 5 &amp;&amp; (fixDigit = fixDigit + 1)

// Replace last two digits with single `fixDigit`
var expectedFraction = fraction.replace(/[d]{2,2}\$/, fixDigit)

return Array(maxValue).fill(0)
.map(function(ignoreValue, index) {
return index + 1
})
.filter(function(integer) {
// Compares 1.015 to 1.0151 b/c fixing by more than one decimal place rounds correctly.
var number = integer + Number(fraction) // number 1.015
var actual = number.toFixed(fixLength)  // string "1.015"
var expected = Number(number + '1').toFixed(fixLength) // string "1.0151"

// Report failures
return expected != actual
})
.map(function(integer) {
// Format reported failures
var number = Number(integer) + Number(fraction)
return {
given: number.toString(),
expected: (Number(integer.toFixed(0)) + Number(expectedFraction)).toString(),
actual: number.toFixed(fixLength)
}
})
}
``````

### Usage

The following example executes on integers 1 through 8, adding the fraction .015 to each, and returns an array of “unexpected” results. Each result contains a `given`, `expected`, and `actual` field. Here we consume the array and print each item.

``````test({ fraction: .015, maxValue: 128 })
.forEach(function(item) {
console.log(item)
})
``````

### Output

For this case, there are 6 unexpected results.

``````Object { given: "1.015", expected: "1.02", actual: "1.01" }
Object { given: "4.015", expected: "4.02", actual: "4.01" }
Object { given: "5.015", expected: "5.02", actual: "5.01" }
Object { given: "6.015", expected: "6.02", actual: "6.01" }
Object { given: "7.015", expected: "7.02", actual: "7.01" }
Object { given: "128.015", expected: "128.02", actual: "128.01" }
``````

## Findings

I found the bug consists of three parts:

1. The last significant digit in the fraction must be 5 (.015 and .01500 produce the same result).
2. The fixing length must shorten the fraction by only one digit.
3. The bug appears inconsistently as different integer values are applied.

## Inconsistently?

For example, `(value).toFixed(2)` with different 3-digit fractions ending in 5, for integers 1 though 128, produces these results:

• fixing numbers ending with .005 ALWAYS fails (!!)
• fixing numbers ending with .015 fails for 1, then 4 through 7, then 128
• fixing numbers ending with .025 fails 1, 2, 3, then 16 through 63
• fixing numbers ending with .0 fails for 1, then 32 through 128
• fixing numbers ending with .045 fails for 1 through 15, then 128
• fixing numbers ending with .055 fails for 1, then 4 through 63
• fixing numbers ending with .065 fails for 1, 2, 3, then 8 through 15, then 32 through 128
• fixing numbers ending with .075 fails for 1, then 8 through 31, then 128
• fixing numbers ending with .085 fails for 1 through 7, then 64 through 127 (!!)
• fixing numbers ending with .095 fails for 1, then 4 through 7, then 16 through 128

Those of you with more binary and floating-point math knowledge than me can probably reason out the underlying cause. I leave that as an exercise for the reader.

## Fixing `toFixed()`

Fixing a value by more than one decimal place always rounds correctly; e.g., `(1.0151).toFixed(2)` returns “1.02” as expected. Both the test and polyfill use that knowledge for their correctness checks.

That means there’s a simple fix for all implementations of `toFixed()`: If the value contains a decimal, append “1” to the end of the string version of the value to be modified. That may not be “to spec,” but it means we will get the results we expect without having to revisit lower-level binary or floating-point operations.

## Polyfill

Until all implementations are modified, you can use the following polyfill to overwrite `toFixed()`, if you’re comfortable doing that (not everyone is).

``````(1.005).toFixed(2) == "1.01" || (function(prototype) {
var toFixed = prototype.toFixed

prototype.toFixed = function(fractionDigits) {
var split = this.toString().split('.')
var number = +(!split[1] ? split[0] : split.join('.') + '1')

}
}(Number.prototype));
``````

Then run the test again and check that the length of the results is zero.

``````test({ fraction: .0015, maxValue: 516 }) // Array []
test({ fraction: .0015, maxValue: 516 }).length // 0
``````

Or just run the initial conversion that started off this post.

``````(1.015).toFixed(2) // returns "1.02" as expected
``````