Dealing with Floating Point Numbers in JavaScript: Lessons Learned
I recently faced a well known issue related to floating point numbers. The experience reminded me of the importance of handling correctly monetary values in JavaScript and to not overlook details.
The problem
A user had reported that when attempting to refund an order with a specific amount, the amount entered was off by one cent. After investigating the issue, I discovered that it was caused by how JavaScript handles floating point numbers.
The IEEE 754 standard is supported by almost every computer through software or hardware. Dedicated hardware to handle floating points was added in 1985 with the Intel 8087 CPU. The standard provides a way to represent a wide range of values, including very large and very small numbers, but it can also lead to rounding errors when working with certain decimal values. Vast majority of programming languages utilize the FPU (Floating Point Unit component) instead of implementing the standard in software.
The number 19.9 was one of those floating point numbers that had this issue (there are numerous more, see title ๐คฃ). Since cents are used in the backend and database, and not decimals, conversion from decimal to cents (integer) was required.
For an example in the BE the value is represented as 1990 and in the FE 19.9 is used for input elements and certain display features.
In this case the refund amount was multiplied by 100 to convert it to cents, the rounding issues caused the return value to be 1989.9999999999998, instead of the expected 1990.
Simple solution
To solve this issue, there were options available:
I could have used (19.9 * 100).toFixed(0)
Or round Math.round(19.9 * 100)
Or rely on a library like decimal.js or big.js
I decided to go with the second approach, as the return value had to be a number still. The first approach required to parse it from string to integer again and the third approach was a big step for a small problem.
Avoiding the issue getting to production
There were unit tests to verify the logic,however it did cover only the happy paths (1 or 2 values) with one invalid value being tested. While it was an OK test coverage it was not enough to reveal the floating point problem. So property testing.
Property testing is a testing method that generates random inputs to test if a program behaves correctly for a range of inputs. By using property testing in addition to unit tests, we can ensure that our code is robust and handles edge cases appropriately. In the case of the decimal precision issue, had we used property testing, we might have discovered the issue earlier.
Here is an example. I am using Vitest as a testing library and Fast-Check for property testing:
// essentially we are using `fc` from `fast-check`, but we have
import { test, fc } from '@fast-check/vitest';
import { it, describe, expect } from 'vitest';
function toCents(value: number) {
return Math.round(value * 100);
}
describe('Money value', () => {
test([fc.float({ min: 1, max: 1000000, noNaN: true })])(
'is converted correctly to cents',
floatValue => {
const value = parseFloat(floatValue.toFixed(3));
// using another approach in the test to get the correct value
// so that we can confirm that `toCents` works correctly
const expectedValue = parseInt((value * 100).toFixed(0));
expect(expectedValue).toEqual(toCents(value));
}
);
}
Conclusion
The lesson to be learned from this experience is that when working with money in JavaScript, itโs important to be aware of the limitations of the IEEE 754 standard and try to prevent similar issue from occurring in the future.
I hope that the post has been helpful and has given some insights on working with floating point numbers. ๐
Top comments (2)
Never store money in floating point numbers. Recipe for disaster. Integers almost always the safest way
That's right!
We do not store floating point numbers in the database, but on the frontend we needed to allow users to change the amount. So for the presentation layer we had to convert from cents to floating point and then when we send the amount, back to the server, to cents.