JavaScript Number Format Table
| Pattern | Output (en-US) | Field note |
|---|---|---|
toFixed(2) | '3.14' | Returns a STRING; 3.145 rounds by binary float, not school rules |
Intl.NumberFormat('en-US').format(12345.6) | '12,345.6' | Locale grouping done right - never regex commas |
style: 'currency', currency: 'USD' | '$12,345.60' | Currency symbol and position come from the locale |
style: 'percent' | '42%' | Expects a FRACTION: 0.42 formats as 42% |
notation: 'compact' | '1.2M' | K/M/B abbreviations - dashboards and social counts |
minimumFractionDigits: 2 | Pads decimals | 3 → '3.00'; pairs with maximumFractionDigits |
Intl.NumberFormat('de-DE').format(12345.6) | '12.345,6' | German swaps dots and commas - never hardcode separators |
Formatting numbers is where JavaScript hides its second famous coercion surprise: toFixed returns a STRING, not a number, and chains silently become concatenations. The table below is the working seven - the legacy method plus the Intl patterns that replaced hand-built comma regexes and hardcoded currency symbols.
Bottom line: Intl.NumberFormat is the only correct way to add grouping, currency or percent - because grouping is not decorative. German 1.234,5 and English 1,234.5 are the same number with opposite separators, and a hand-rolled comma regex will ship the wrong format to a German user. Build one formatter per locale-and-style, reuse it everywhere, and let the browser own the separators.
The honest part: two type-safety notes cover most formatting bugs. toFixed rounds by binary float, so 3.145.toFixed(2) can surprise school-arithmetic expectations (the stored double is slightly below the decimal). And percent style expects a FRACTION - 0.42 formats as 42%, which means dividing your percentage by 100 before formatting, not after.
How to use
- Pick the pattern by output: plain decimals (toFixed), locale grouping (Intl default), currency, percent, or compact notation.
- Create the formatter once per locale-and-style at module level - construction parses locale data and is the expensive part.
- Remember toFixed's return is a string: anything numeric happening after formatting happens before it, never after.
Frequently asked questions
Why does toFixed return a string instead of a number?
Because a formatted number is not a number anymore: '3.14' is three characters, and adding it to something concatenates instead of adds. JavaScript keeps the distinction strict so the display form never leaks back into arithmetic. The production pattern: do all math on numbers, convert to string exactly once at the display boundary - and if you need the value back, Number(formatted) round-trips (minus the separators that Intl adds).
Should I add thousand separators with a regex?
No - that is the last legitimate use case Intl.NumberFormat killed. A comma regex breaks internationally (German writes 1.234,5), breaks on decimals (commas after the decimal point are wrong), and usually breaks on negative numbers or very large values. new Intl.NumberFormat('en-US').format(n) does the grouping, handles negatives, and respects the user's locale when you pass undefined instead of a hard-coded locale.
Why does percent style show 4200% when I pass 42?
Because percent style expects a FRACTION: the formatter multiplies by 100 and appends the sign, so 0.42 formats as 42%. Passing your already-multiplied 42 produces 4,200%. The fix is at the input: divide by 100 before formatting, or store fractions natively and let the formatter do the multiplication - the latter survives unit changes without re-auditing every call site.
Is constructing Intl.NumberFormat repeatedly slow?
Yes enough to matter in loops: each constructor validates locale data and builds an internal table, and formatting thousands of rows with a per-row constructor measurably drags. The fix is memoization - create one formatter per locale-and-style at module level and reuse it (the instances are stateless for format()). A small Map keyed by the options string is the standard lazy-cache pattern; it typically turns per-row formatting from the slowest line in a render into a rounding error.