Why JavaScript’s toFixed Fails at Precise Rounding and How to Fix It
JavaScript’s built‑in toFixed cannot reliably perform true rounding due to binary floating‑point precision limits, so this article explains the underlying IEEE‑754 representation, demonstrates common pitfalls, and provides a custom rounding algorithm with code examples to achieve accurate decimal rounding.
Current Issues
Problem: toFixed can satisfy only some decimal rounding. According to MDN, Number.prototype.toFixed() definition.
2.55.toFixed(1) // returns '2.5' // Note it rounds down – see warning aboveWarning: floating‑point numbers cannot precisely represent all decimals in binary. This may lead to unexpected results such as 0.1 + 0.2 === 0.3 returning false .
MDN states that floating‑point decimal calculations may be abnormal, so toFixed cannot meet strict rounding requirements.
Why Not Use the Following Method for Rounding
const round = (num: number, decimal = 2): string => {
const rate = 10 ** decimal;
const temp = Math.round(num * rate) / rate;
let strNum = String(temp);
const numArr = strNum.split('.');
if (!numArr[1]) {
strNum += '.';
strNum = strNum.padEnd(strNum.length + decimal, '0');
} else if (numArr[1].length < decimal) {
strNum = strNum.padEnd(numArr[0].length + 1 + decimal, '0');
}
return strNum;
};The core of this approach is Math.round(num * 10 ** decimal) / 10 ** decimal. It works for most scenarios but has two issues:
If num itself does not exceed Number.MAX_SAFE_INTEGER but num * rate does, unexpected cases may occur.
Some cases, such as rounding 1.255 to two decimal places, still suffer from precision loss.
Specific Reason Why 0.1 + 0.2 !== 0.3
All numbers in JavaScript, including integers and decimals, are of a single type – Number . Its implementation follows the IEEE‑754 standard, using a 64‑bit double‑precision floating‑point format.
The calculation process includes:
Decimal to Binary
Convert 0.1 to binary (illustrated below):
The conversion results in an infinite repeating pattern:
0.0001100110011001100110011001100110011001100110011001101...Binary to Scientific Notation
0.1 becomes 1.1(0011)… × 2⁻⁴ (the binary point moves four places to the right).
Binary Representation of Scientific Notation
The 64‑bit layout consists of a sign bit, 11 exponent bits, and 52 mantissa bits. For 0.1 the binary representation is:
0 01111111011 1001100110011001100110011001100110011001100110011010Similarly, 0.2 is:
0 01111111100 1001100110011001100110011001100110011001100110011010Exponent Alignment (对阶运算)
To add the numbers, exponents must be aligned (the smaller exponent is shifted to match the larger one). After aligning to -3, 0.1 becomes:
0 01111111100 (0.)1100110011001100110011001100110011001100110011001101Binary Addition
Rounding
The result has two problems:
It does not conform to scientific‑notation rules.
The mantissa exceeds the allowed 52 bits.
Adjusting the result by shifting the decimal point left yields: 1.00110011001100110011001100110011001100110011001100111 Increasing the exponent by 1 gives 01111111101. After rounding the excess mantissa bits, the final binary is: 1.0011001100110011001100110011001100110011001100110100 Converting back to decimal produces 0.30000000000000004, demonstrating inevitable precision loss.
How to Avoid This Issue with a Rounding Function
Because rounding is common in statistical calculations, a custom function can achieve perfect rounding by:
Convert the number into an array of digits.
Start from the last digit and check if it is greater than 4.
If ≤4, break.
If >4, move left and add 1.
Check whether the increment results in 10.
If not 10, break.
If it becomes 10, set the digit to 0 and record a carry to the next higher digit (prepend 1 if at the most significant position).
Continue the loop until no further carry.
// ...
// core code
// match all digits into an int[] e.g., 1.223 → [1,2,2,3]
const numArr = zeroStrNum.match(/\d/g) || [];
// start from the last digit
if (parseInt(numArr[numArr.length - 1], 10) > 4) {
for (let i = numArr.length - 2; i >= 0; i--) {
numArr[i] = String(parseInt(numArr[i], 10) + 1);
if (numArr[i] === '10') {
numArr[i] = '0';
flag = i !== 1; // carry to previous digit
} else {
break;
}
}
}
// ...References:
https://zhuanlan.zhihu.com/p/103254614
https://zhuanlan.zhihu.com/p/363254961
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed
https://www.boatsky.com/blog/26
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
