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.

ELab Team
ELab Team
ELab Team
Why JavaScript’s toFixed Fails at Precise Rounding and How to Fix It

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 above
Warning: 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 1001100110011001100110011001100110011001100110011010

Similarly, 0.2 is:

0 01111111100 1001100110011001100110011001100110011001100110011010

Exponent 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.)1100110011001100110011001100110011001100110011001101

Binary 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

PrecisionRoundingtoFixedIEEE-754floating-point
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.