跪拜 Guibai
← All articles
Frontend · JavaScript · Interview

Five JavaScript Number Traps That Break Production Without a Single Error

By kyriewen ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

These traps corrupt financial data, break API lookups, and produce wrong UI state without a single console error or exception. A developer who relies on toFixed for currency display or parses a 19-digit order ID as a number will ship broken behavior that only surfaces in production, often as incorrect charges or missing records.

Summary

`toFixed` rounds against the binary representation, not the decimal literal, so (1.005).toFixed(2) returns "1.00" instead of "1.01". JSON.parse silently corrupts integers beyond 2^53-1, rewriting the last digits of snowflake IDs with no warning. NaN's type is 'number', it is not equal to itself, and Number('') returns 0, turning empty form fields into zero-value calculations that cascade through multiplication.

parseInt(0.0000005) evaluates to 5 because the argument is first coerced to the string "5e-7", and parsing stops at 'e'. Floating-point errors accumulate across loops, so splitting 100 yuan three ways and summing the rounded parts yields 99.99, not 100. The root cause across all five traps is the same: JavaScript's IEEE 754 double-precision floats silently produce wrong results instead of throwing exceptions.

The fixes are concrete. Use Intl.NumberFormat for display rounding, not toFixed. Treat all IDs as strings end-to-end, or use BigInt with a custom JSON reviver. Validate numeric inputs with a safe-number function that explicitly handles empty strings. Store and calculate all monetary values as integer cents, distributing remainders explicitly. These three habits—string IDs, integer cents, validated inputs—eliminate the entire class of silent number bugs.

Takeaways
toFixed rounds the binary floating-point value, not the decimal literal, so (1.005).toFixed(2) returns "1.00".
Multiplying by 100 and using Math.round fails for the same reason: 1.005 * 100 equals 100.49999999999999.
Intl.NumberFormat handles display rounding correctly and should replace toFixed for all presentation-layer formatting.
JSON.parse silently corrupts integers larger than Number.MAX_SAFE_INTEGER (9,007,199,254,740,991), rewriting the last digits of 19-digit snowflake IDs.
The only safe ways to handle large IDs are to have the backend serialize them as strings, use BigInt with a custom JSON reviver, or use a library like json-bigint.
NaN's type is 'number', NaN !== NaN, and Number('') returns 0, so empty form fields become zero in calculations without any error.
A safe-number validation function must explicitly check for empty strings because Number.isFinite(0) is true.
parseInt(0.0000005) returns 5 because the number is first coerced to the string "5e-7", and parseInt stops at 'e'.
Math.round(-0.5) returns -0 because rounding is toward positive infinity, and -7 % 3 returns -1 because the modulo operator preserves the dividend's sign.
Floating-point errors accumulate in loops: summing 0.1, 0.2, and 0.3 with reduce does not equal 0.6, and (100 / 3).toFixed(2) * 3 returns 99.99.
All monetary calculations should use integer cents, with explicit remainder distribution to ensure totals never change.
For complex currency needs, dinero.js and decimal.js are actively maintained options; for simple formatting, Intl.NumberFormat alone is sufficient.
Conclusions

The common thread across all five traps is not floating-point imprecision itself, but JavaScript's design choice to silently produce wrong results instead of throwing errors at the boundaries.

toFixed is fundamentally broken for financial display because it operates on the stored binary value, not the decimal literal—a mismatch between the method's stated purpose and its actual behavior.

The parseInt(0.0000005) === 5 case reveals a deeper problem: implicit type coercion chains that are invisible during code review and produce results so counterintuitive they look like cosmic rays.

Integer-cents arithmetic is not a workaround but the correct domain model for money—floating-point was never the right representation, and the 'fix' is simply using the right type.

The fact that NaN !== NaN is specified by IEEE 754 and is not a JavaScript quirk, but its interaction with Number('') returning 0 creates a uniquely dangerous silent-failure path in form handling.

Concepts & terms
IEEE 754 double-precision floating-point
The binary format JavaScript uses for all numbers. It can exactly represent integers only up to 2^53-1 (Number.MAX_SAFE_INTEGER). Beyond that, gaps between representable integers grow, causing precision loss. Decimal fractions like 0.1 have no exact binary representation, producing small rounding errors that accumulate across operations.
Snowflake ID
A distributed unique ID generation algorithm, originally from Twitter, that produces 64-bit integers typically 17-19 digits long. These IDs exceed JavaScript's safe integer range, so they must be handled as strings in JavaScript to avoid silent truncation.
Intl.NumberFormat
A built-in JavaScript internationalization API that formats numbers according to locale-specific rules. Unlike toFixed, it correctly handles decimal rounding for display purposes and supports currency, percentage, and unit formatting.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗