JavaScript has several built-in and third-party ways to format a number as currency. Here are the ones worth knowing, and when to reach for each.
1. toLocaleString: built into JavaScript, formats a number according to the given locale:
const number = 1234567.89;
const formattedCurrency = number.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
console.log(formattedCurrency); // Output: $1,234,567.89
2. Intl.NumberFormat: part of the ECMAScript Internationalization API, gives more control than toLocaleString and is the modern standard for this:
const number = 1234567.89;
const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
const formattedCurrency = formatter.format(number);
console.log(formattedCurrency); // Output: $1,234,567.89
3. Manual formatting: if you need full control and don’t want to depend on locale data:
const number = 1234567.89;
const formattedCurrency = `$${number.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,')}`;
console.log(formattedCurrency); // Output: $1,234,567.89
4. External libraries: numeral.js or accounting.js, if you need formatting features beyond what the built-ins offer:
// numeral.js
const number = 1234567.89;
const formattedCurrency = numeral(number).format('$0,0.00');
// accounting.js
const formattedCurrency2 = accounting.formatMoney(number, '$', 2);
Where this actually matters: not every currency formats like USD. Japanese Yen has no decimal places; some European formats use a comma as the decimal separator and a period for thousands. Hardcoding $ and comma-formatting (the manual approach) breaks silently for these — Intl.NumberFormat handles it correctly because it’s locale-aware, not just symbol-aware:
new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(1234567.89);
// ¥1,234,568 -- no decimals, correctly rounded
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234567.89);
// 1.234.567,89 € -- comma as decimal separator, period for thousands, symbol after the number
The manual regex approach from option 3 gets none of this right without rewriting the logic per currency — which is exactly the maintenance cost of avoiding the built-in API.
The real trap: doing math on money as a JS number
Everything above is display formatting — turning a number into a string for humans to read. It’s a separate, more consequential problem if you’re doing arithmetic on money values using plain JavaScript numbers, because floating-point can’t represent most decimal fractions exactly:
console.log(0.1 + 0.2); // 0.30000000000000004, not 0.3
console.log(19.99 * 3); // 59.96999999999999, not 59.97
For display, that’s invisible — Intl.NumberFormat rounds to 2 decimals and the tiny error disappears. But add up enough transactions, or compare a calculated total against a stored one with ===, and that sub-cent drift becomes a real bug: totals that are off by a cent, or an equality check that fails when it visually shouldn’t. The standard fix is to never store or calculate money as a fractional number — work in integer cents instead (1999 instead of 19.99), do all arithmetic on the integer, and only convert to a decimal-formatted string at the very last step, for display:
const priceInCents = 1999; // $19.99, stored as an integer
const quantity = 3;
const totalCents = priceInCents * quantity; // 5997 -- exact, no floating-point drift
const formatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
.format(totalCents / 100); // convert only at the display step
// "$59.97"
For anything beyond simple arithmetic — proper rounding rules, multi-currency conversion math — a dedicated library like dinero.js or currency.js handles this more rigorously than hand-rolled integer math, but the underlying principle is the same one either way: format for display, never compute on the formatted (or raw floating-point) value.
Multi-currency integer math: minor units aren’t always 2 decimals
The “store as integer cents, divide by 100 at display” pattern above has a hidden assumption: that a currency’s minor unit is always 100ths. It isn’t. ISO 4217 defines the minor-unit digit count per currency, and it varies — JPY, KRW, VND, and CLP have zero minor-unit digits (there’s no such thing as “yen cents”), while a handful of currencies like BHD, KWD, and OMR use three. Divide a JPY integer by 100 assuming it’s “cents” and every amount is off by a factor of 100; the bug won’t show up in testing if your test data happens to be USD.
Intl.NumberFormat already knows the correct digit count per currency — pull it from resolvedOptions() instead of hardcoding a divisor:
function getMinorUnitDigits(currency) {
return new Intl.NumberFormat('en-US', { style: 'currency', currency })
.resolvedOptions().maximumFractionDigits;
}
getMinorUnitDigits('USD'); // 2
getMinorUnitDigits('JPY'); // 0
getMinorUnitDigits('BHD'); // 3
function toDisplayAmount(minorUnits, currency) {
const digits = getMinorUnitDigits(currency);
const amount = minorUnits / Math.pow(10, digits);
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}
toDisplayAmount(1999, 'USD'); // "$19.99"
toDisplayAmount(1999, 'JPY'); // "¥1,999" -- not "¥19.99"
Worth checking for specifically if your app handles more than one currency, or ever will — this is exactly the kind of bug that survives code review because the math is correct for whichever currency the reviewer happened to picture.
In this post: The real trap: doing math on money · Multi-currency integer math · Controlling the rounding mode · Which one should you actually use? · Related reading
Intl.NumberFormat gives more explicit control than toLocaleString and is the modern standard for this.
Controlling the rounding mode
Worth knowing this exists, since the default doesn’t match what financial systems often actually require: `Intl.NumberFormat`’s `roundingMode` option, a current, verified addition to the spec, lets you pick from `”ceil”`, `”floor”`, `”expand”`, `”trunc”`, and several `”half*”` variants — `”halfExpand”` (round half away from zero) is the default if you don’t specify one. `”halfEven”` implements banker’s rounding specifically: ties round toward whichever neighbor is even, which is the rounding rule many accounting and financial-reporting systems require precisely because it doesn’t systematically bias sums upward the way “always round 0.5 up” does over many transactions:
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', roundingMode: 'halfExpand' }).format(2.225);
// "$2.23" -- default: rounds the tie up
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', roundingMode: 'halfEven' }).format(2.225);
// "$2.22" -- banker's rounding: ties toward the even neighbor
Worth confirming which rounding rule a specific financial requirement actually calls for before assuming the default is fine — it’s exactly the kind of detail that looks identical in casual testing and only diverges once real transaction volume makes the systematic bias visible in a reconciliation report.
Which one should you actually use?
- You only need USD formatted for a modern browser —
toLocaleStringworks with zero dependencies. - You need consistent formatting across multiple currencies or locales —
Intl.NumberFormatis more explicit and reliable thantoLocaleString‘s locale handling. - You can’t rely on the Internationalization API being available (older environments) — manual formatting with
toFixedand a regex is your fallback, but you lose proper locale/currency-symbol handling. - You’re already pulling in numeral.js or accounting.js elsewhere in the codebase — stick with it for consistency rather than mixing formatting approaches.
Related reading
- Store and Display Dates Properly in JavaScript — the same “don’t trust the default formatting” problem, applied to dates instead of currency.
- Handling International Date Functionalities in JavaScript — locale handling for dates, the counterpart to the Intl.NumberFormat locale handling covered here.
Hope that saves you some time — drop a comment if anything’s unclear.
App Catalog Authentication Automation Backup Compliance Content Type CSS Flows Google Javascript Limitations List Metadata MFA Microsoft Node NodeJs O365 OneDrive Permissions PnP PnPJS Policy PowerApps Power Automate PowerAutomate PowerPlatform PowerShell React ReactJs Rest API Rest Endpoint Security Send an HTTP Request to SharePoint SharePoint SharePoint List SharePoint Modern SharePoint Online SPFX SPO Sync Tags Teams Termstore Versioning


