JavaScript Number Format Table

PatternOutput (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: 2Pads decimals3 → '3.00'; pairs with maximumFractionDigits
Intl.NumberFormat('de-DE').format(12345.6)'12.345,6'German swaps dots and commas - never hardcode separators
Reference: the MDN Intl.NumberFormat reference. Three facts prevent most formatting bugs: toFixed returns a STRING (3.14 + 0 after toFixed is string concatenation waiting to happen), percent style expects a fraction (0.42, not 42), and locale grouping is not decorative - German 1.234,5 and English 1,234.5 are the same number with opposite separators, which is why hand-built comma regexes break internationally. Bottom line: create ONE NumberFormat instance per locale/style and reuse it - construction is expensive, formatting is cheap. Related tools: JS array methods table, equality table for the float comparisons after rounding, and URL API table for the other built-in that replaced hand-rolled string work.

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

  1. Pick the pattern by output: plain decimals (toFixed), locale grouping (Intl default), currency, percent, or compact notation.
  2. Create the formatter once per locale-and-style at module level - construction parses locale data and is the expensive part.
  3. 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.

Related tools