Call toLocaleString with an explicit locale: (1234567.891).toLocaleString('en-US') returns 1,234,567.891. For money, or any format you use more than once, build an Intl.NumberFormat: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n) gives $1,234,567.89.
JavaScript has a full number formatter built in: Intl.NumberFormat, which Number.prototype.toLocaleString uses under the hood. It knows every locale's grouping and decimal characters, every currency's symbol and number of decimals, and compact forms like 1.2K. So the old hand-rolled regex is now only a fallback. The one habit to build is always passing a locale: without one you get whatever locale the machine running the code is set to. Each example runs on this page: hit Run, then edit the code and run it again.
1toLocaleString('en-US')Recommended
Every number has a toLocaleString(locales, options) method that returns a formatted string. With 'en-US' you get commas between groups of three digits and a dot for the decimal point. It handles negatives and large integers, and it needs no setup, which makes it the right call for formatting a single value.
Output
Prints 1,234,567.891, 9,876,543,210 and -42,000, then 0.123 and 1,000.5. Two defaults are worth knowing: at most three decimals are kept (the rest are rounded away), and none are added, so 1000.5 is not padded to 1,000.50. Section 2 fixes both. Leaving out the locale, as in n.toLocaleString(), uses the default locale of the runtime (the browser's language or the server's OS settings), so the same line prints 1.234.567,891 for a user in Germany. That is right for text shown to that user and wrong for anything stored, logged or tested.
2A reusable Intl.NumberFormat (and fixed decimals)
new Intl.NumberFormat(locale, options) does the locale and option lookup once and gives you a formatter whose format(n) method you can call as often as you like. The spec has toLocaleString build a new formatter on every call, so in a loop over a table of rows the formatter is the better choice. The same options work on both. To always show a set number of decimals, set minimumFractionDigits and maximumFractionDigits to the same value.
Output
The loop prints 999, 1,000, 25,000.5 and 1,234,567. The two-decimal formatter prints 1,234.50, 1,234.57 and 7.00: it pads short values and rounds long ones. The last line, 1234.50, is why toFixed(2) is not the answer here: it fixes the decimals but never adds separators. If you only want to limit the decimals, set just maximumFractionDigits.
3Format numbers as currency
Add style: 'currency' and an ISO 4217 currency code, and Intl.NumberFormat picks the symbol, where it goes, how negatives look and how many decimals the currency uses. The two settings are independent: the locale decides the layout (separators, symbol position), and the currency decides the symbol and the number of minor-unit digits. currency is required with this style; there is no default.
Output
Prints $1,234,567.89 and -$5.50 for US dollars, then 1.234.567,89 € for euros in German layout, ¥1,234,568 for yen (zero decimals, so it rounds to a whole yen), and EUR 1,234.50 with the code instead of the symbol. The final true is the thing that trips up tests: the gap between number and symbol is a no-break space (U+00A0), so comparing against a string typed with a normal space fails. Store money as integer cents or a decimal string and format it only for display.
4Other locales' separators
"Commas" is an English convention. Germany swaps the roles of comma and dot, France groups with a space, Switzerland with an apostrophe, and India groups the first three digits and then pairs (lakh and crore). You don't need to know any of that: pass the user's locale and the formatter applies its rules.
Output
The loop prints 1,234,567.891 (en-US), 1.234.567,891 (de-DE), 1 234 567,891 (fr-FR), 1'234'567.891 (de-CH) and 12,34,567.891 (en-IN). The 2 confirms that French groups with a narrow no-break space (U+202F), not a regular space. The en-IN line after it shows the lakh pattern more clearly: 12,34,56,789. With useGrouping: false you get 1234567.891, the locale's decimal character without any group separators.
5Compact notation (1.2K) and BigInt
For follower counts and dashboards, notation: 'compact' abbreviates large numbers the way the locale does (K, M, B in English). At the other end, a Number is only exact up to Number.MAX_SAFE_INTEGER (about 9 quadrillion). Past that, keep the value as a BigInt, which has its own toLocaleString, or as a decimal string, which format() accepts directly.
Output
Compact prints 999, 1.2K, 57K, 3.4M and 5.6B, then 3.4 million in the long form. It rounds hard by default (56,789 becomes 57K); maximumFractionDigits: 1 gives 56.8K. The BigInt prints exactly as 12,345,678,901,234,567,890, while the same value converted to a Number prints 12,345,678,901,234,567,000: the last digits were lost in the conversion, not in the formatting. The decimal string comes out whole as 98,765,432,109,876,543,210.25.
6The regex fallback (and its limits)
Before Intl was everywhere, the standard answer was a regular expression that inserts a comma at every position followed by a multiple of three digits. It still works for plain integers and it only ever produces US-style commas. It is worth recognising in old code, and worth knowing where it breaks before you copy it.
Output
Integers work: 1,234,567 and -9,876,543. But the lookahead can't tell the integer part from the fraction, so 1234.5678 becomes 1,234.5,678. Splitting on the dot first gives 1,234.5678. The second limit is in String(n) itself: from 1e21 upward it returns exponent notation, so the regex sees 1e+21 and has nothing to group, while toLocaleString prints 1,000,000,000,000,000,000,000. There is also no rounding, no currency and no other locale. Use it only where Intl is genuinely unavailable.
7Which should you use?
| Method | Separators | Decimals | Best for |
|---|---|---|---|
| n.toLocaleString('en-US') | Any locale | Up to 3, rounded | Formatting one value |
| new Intl.NumberFormat('en-US', opts) | Any locale | You choose | Loops, fixed decimals, reuse |
| { style: 'currency', currency } | Any locale | Per currency (USD 2, JPY 0) | Prices and totals |
| { notation: 'compact' } | Abbreviated | Rounded short | Counts and dashboards (1.2K) |
| n.toFixed(2) | None | Exactly n | Machine output, not display |
| Regex replace | Commas only | Breaks unless split | Only when Intl is unavailable |
Frequently asked questions
How do I format a number with commas as thousands separators in JavaScript?
Use n.toLocaleString('en-US'), which turns 1234567.891 into 1,234,567.891. Always pass the locale: without it the output follows the machine's settings and can come back as 1.234.567,891. If you format many values, create one new Intl.NumberFormat('en-US') and call its format() method.
How do I format a number as a currency string?
Use new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(1234.5), which returns $1,234.50. Change currency for the symbol and decimals (JPY has none) and the locale for the layout: de-DE with EUR gives 1.234.567,89 € (the gap is a no-break space). The currency option is required when the style is currency.
Why does toLocaleString not add any commas?
Usually because the value is a string, not a number: '1234567'.toLocaleString('en-US') returns 1234567 unchanged, since a string just returns itself from toLocaleString. Convert first with Number('1234567').toLocaleString('en-US'), which gives 1,234,567. Values from inputs, JSON and query strings are often strings.
How do I always show 2 decimal places with commas?
Pass both digit options: new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(1234.5) returns 1,234.50. toFixed(2) gives the two decimals but no separators (1234.50), and the currency style already uses two decimals for USD and EUR.
How do I convert a formatted string like "1,234.56" back to a number?
Remove the separators, then convert: Number('1,234.56'.replaceAll(',', '')) is 1234.56. Number('1,234.56') on its own is NaN, and parseFloat('1,234.56') quietly stops at the comma and returns 1. For other locales you have to know which character is the group separator and which is the decimal before you strip anything.
Why does my formatted euro amount not equal the string I expected?
Intl puts a no-break space (U+00A0) between the amount and the euro sign in de-DE, and French grouping uses a narrow no-break space (U+202F). So format(5) === '5,00 €' typed with a normal space is false, while '5,00\u00A0€' is true. In tests, compare against a string built with \u00A0, or normalise with .replace(/\s/g, ' '), which matches both characters.
Run it yourself
Open any of these in the full JavaScript editor — tweak, run, and share.
JavaScript playground