dinero.js vs currency.js vs Intl.NumberFormat: Money in JavaScript (2026)
TL;DR
Never use floating-point arithmetic for money — 0.1 + 0.2 !== 0.3 in JavaScript. dinero.js is the most complete monetary value library — stores amounts as integers (cents), handles multi-currency arithmetic, and supports complex rounding strategies used in financial software. currency.js is the lightweight alternative — a 1KB library with a simple, chainable API using integer arithmetic internally. Intl.NumberFormat is the browser/Node.js built-in for formatting — excellent for display but not for arithmetic. For any app handling payments or financial data: dinero.js v2 or currency.js. For display-only formatting: Intl.NumberFormat (no library needed).
Key Takeaways
- dinero.js: ~1.5M weekly downloads — full monetary library, integer arithmetic, multi-currency, complex rounding
- currency.js: ~1.5M weekly downloads — 1KB, chainable API, simple and focused
- Intl.NumberFormat: Built-in (no install) — formatting only, NOT for arithmetic
0.1 + 0.2 = 0.30000000000000004in JavaScript — always use integer cents internally- dinero.js v2 is a full rewrite — functional API, immutable, tree-shakable, multi-precision
- Stripe and most payment APIs use integers (cents) — match their representation
The Floating Point Problem
// Why you cannot use regular numbers for money:
console.log(0.1 + 0.2) // 0.30000000000000004
console.log(1.005 * 100) // 100.50000000000001 (rounding error)
console.log(1.15 * 10) // 11.499999999999998
// Real impact:
const price = 19.99
const tax = price * 0.08 // 8% tax
console.log(tax) // 1.5992000000000002
console.log(price + tax) // 21.589200000000003
// The correct approach: store everything in cents (integers)
const priceInCents = 1999 // $19.99
const taxInCents = Math.round(priceInCents * 0.08) // 160 cents = $1.60
const totalInCents = priceInCents + taxInCents // 2159 = $21.59
dinero.js v2
dinero.js — the professional monetary value library:
Basic usage
import { dinero, add, subtract, multiply, divide, toSnapshot } from "dinero.js"
import { USD, EUR, GBP } from "@dinero.js/currencies"
// Create a monetary value (always in subunit — cents for USD):
const price = dinero({ amount: 1999, currency: USD })
// Represents $19.99 (1999 cents, USD has 2 decimal places)
const tax = dinero({ amount: 160, currency: USD })
// $1.60
// Arithmetic (all immutable — returns new Dinero object):
const total = add(price, tax)
// Dinero { amount: 2159, currency: USD }
const discounted = subtract(price, dinero({ amount: 500, currency: USD }))
// $14.99
const doubled = multiply(price, 2)
// $39.98
Formatting for display
import { dinero, toDecimal, toUnit } from "dinero.js"
import { USD, EUR, JPY } from "@dinero.js/currencies"
const price = dinero({ amount: 1999, currency: USD })
const euros = dinero({ amount: 1999, currency: EUR })
const yen = dinero({ amount: 1999, currency: JPY }) // JPY has 0 decimal places
// Convert to decimal string:
toDecimal(price) // "19.99"
toDecimal(euros) // "19.99"
toDecimal(yen) // "1999" (JPY has no subunit)
// Convert to number (use carefully — floating point!):
toUnit(price) // 19.99
// Format with Intl.NumberFormat (recommended for display):
import { USD } from "@dinero.js/currencies"
function formatMoney(amount: number, currencyCode: string, locale = "en-US") {
return new Intl.NumberFormat(locale, {
style: "currency",
currency: currencyCode,
}).format(amount / 100)
}
// Or use toDecimal with transformer:
toDecimal(price, ({ value, currency }) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: currency.code,
}).format(Number(value))
)
// "$19.99"
Rounding strategies
import { dinero, divide, up, down, halfUp, halfDown, halfEven } from "dinero.js"
import { USD } from "@dinero.js/currencies"
const price = dinero({ amount: 100, currency: USD }) // $1.00
// Divide $1.00 by 3 = 33.333...
divide(price, 3, up) // $0.34 (round up)
divide(price, 3, down) // $0.33 (round down / truncate)
divide(price, 3, halfUp) // $0.33 (half rounds up → 0.333... rounds down)
divide(price, 3, halfEven) // $0.33 (banker's rounding — rounds to nearest even)
// halfEven (banker's rounding) is used in financial systems to avoid systematic bias:
// $2.50 split in half → $1.25 (rounds to even = up)
// $1.50 split in half → $1.50 → $0.75 (rounds to even = down for the half)
Multi-currency and comparison
import { dinero, add, equal, greaterThan, lessThan, toSnapshot } from "dinero.js"
import { USD, EUR } from "@dinero.js/currencies"
const usdAmount = dinero({ amount: 1999, currency: USD })
const eurAmount = dinero({ amount: 1999, currency: EUR })
// Comparison (same currency):
const a = dinero({ amount: 1000, currency: USD })
const b = dinero({ amount: 2000, currency: USD })
equal(a, b) // false
greaterThan(b, a) // true
lessThan(a, b) // true
// Cannot mix currencies — you handle conversion first:
// (dinero doesn't do FX conversion — use an FX rates API)
function convertUSDToEUR(usd: ReturnType<typeof dinero>, rate: number) {
const { amount } = toSnapshot(usd)
return dinero({ amount: Math.round(amount * rate), currency: EUR })
}
// Serialize and restore:
const snapshot = toSnapshot(usdAmount)
// { amount: 1999, currency: { code: "USD", base: 10, exponent: 2 }, scale: 0 }
// Store in database as { amount: 1999, currency: "USD" }
// Restore with:
const restored = dinero({ amount: snapshot.amount, currency: USD })
Cart total example
import { dinero, add, multiply, toDecimal } from "dinero.js"
import { USD } from "@dinero.js/currencies"
interface CartItem {
name: string
priceInCents: number
quantity: number
}
function calculateCartTotal(items: CartItem[]) {
const zero = dinero({ amount: 0, currency: USD })
const subtotal = items.reduce((acc, item) => {
const itemPrice = dinero({ amount: item.priceInCents, currency: USD })
const lineTotal = multiply(itemPrice, item.quantity)
return add(acc, lineTotal)
}, zero)
const { amount: subtotalAmount } = toSnapshot(subtotal)
const taxAmount = Math.round(subtotalAmount * 0.08) // 8% tax
const tax = dinero({ amount: taxAmount, currency: USD })
const total = add(subtotal, tax)
return {
subtotal: toDecimal(subtotal), // "39.98"
tax: toDecimal(tax), // "3.20"
total: toDecimal(total), // "43.18"
}
}
currency.js
currency.js — 1KB, chainable, simple:
Basic usage
import currency from "currency.js"
// Create:
const price = currency(19.99) // Stores as cents internally
const price2 = currency("$19.99") // Parses string (strips $ and commas)
const price3 = currency(1999, { fromCents: true }) // From cents
// Arithmetic (chainable, immutable):
currency(19.99).add(1.60) // $21.59
currency(19.99).subtract(5.00) // $14.99
currency(19.99).multiply(2) // $39.98
currency(100).divide(3) // $33.33 (rounds to 2 decimal places)
// Chained:
const total = currency(19.99)
.add(1.60) // Add tax
.subtract(2.00) // Subtract discount
console.log(total.value) // 19.59 (as JavaScript number)
console.log(total.intValue) // 1959 (as integer cents)
console.log(total.format()) // "$19.59"
Formatting options
import currency from "currency.js"
// Default (USD):
currency(1234567.89).format() // "$1,234,567.89"
// Euro:
const EUR = (value: number | string) => currency(value, {
symbol: "€",
separator: ".",
decimal: ",",
})
EUR(1234.56).format() // "€1.234,56"
// Japanese Yen (no decimals):
const JPY = (value: number | string) => currency(value, {
symbol: "¥",
precision: 0,
separator: ",",
decimal: ".",
})
JPY(1234567).format() // "¥1,234,567"
// No symbol (for APIs):
currency(19.99, { symbol: "" }).format() // "19.99"
Real-world usage
import currency from "currency.js"
// Cart total:
const items = [
{ price: 19.99, quantity: 2 },
{ price: 5.50, quantity: 3 },
]
const subtotal = items.reduce(
(total, item) => total.add(currency(item.price).multiply(item.quantity)),
currency(0)
)
// currency(39.98 + 16.50) = currency(56.48)
const tax = subtotal.multiply(0.08)
const total = subtotal.add(tax)
console.log(subtotal.format()) // "$56.48"
console.log(tax.format()) // "$4.52"
console.log(total.format()) // "$61.00"
// Parse user input:
const userInput = "$1,234.56"
const amount = currency(userInput)
console.log(amount.intValue) // 123456
console.log(amount.value) // 1234.56
Intl.NumberFormat (built-in)
No install needed — use for formatting money to display to users:
Currency formatting
// Format for display — DO NOT use for arithmetic:
// USD:
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(19.99)
// "$19.99"
// EUR (German locale):
new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(1234.56)
// "1.234,56 €"
// JPY:
new Intl.NumberFormat("ja-JP", {
style: "currency",
currency: "JPY",
}).format(1999)
// "¥1,999"
// Compact (large numbers):
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
notation: "compact",
}).format(1_234_567)
// "$1.2M"
// Reuse formatter instance (performance):
const usdFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
})
prices.map(p => usdFormatter.format(p / 100)) // Convert cents to dollars for display
Useful options
// Minimum/maximum digits:
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(19.9)
// "$19.90"
// Accounting style (negative = parentheses):
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
currencySign: "accounting",
}).format(-1234.56)
// "($1,234.56)"
// Parts (build custom display):
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).formatToParts(1234.56)
// [
// { type: "currency", value: "$" },
// { type: "integer", value: "1" },
// { type: "group", value: "," },
// { type: "integer", value: "234" },
// { type: "decimal", value: "." },
// { type: "fraction", value: "56" },
// ]
Feature Comparison
| Feature | dinero.js | currency.js | Intl.NumberFormat |
|---|---|---|---|
| Integer arithmetic | ✅ | ✅ | ❌ (display only) |
| Multi-currency | ✅ | ❌ | ✅ (formatting only) |
| Rounding strategies | ✅ Multiple | ✅ | ❌ |
| Chainable API | ❌ (functional) | ✅ | ❌ |
| Currency formatting | Via Intl | ✅ | ✅ |
| Bundle size | ~9KB | ~1KB | Built-in (0KB) |
| TypeScript | ✅ | ✅ | ✅ |
| ESM | ✅ | ✅ | N/A |
| Locale-aware | Via Intl | ✅ (symbol) | ✅ Full |
| Install required | ✅ | ✅ | ❌ |
When to Use Each
Choose dinero.js if:
- Building financial software, e-commerce, or any app where precision matters
- Need complex rounding strategies (banker's rounding, round up/down)
- Working with multiple currencies in the same codebase
- Want a fully-typed, immutable, functional approach to money
Choose currency.js if:
- Simple money arithmetic without multi-currency complexity
- Chainable API is preferred over functional style
- Bundle size matters — 1KB vs 9KB for dinero.js
- Quick implementation without reading extensive docs
Use Intl.NumberFormat if:
- Only need to format monetary values for display (no arithmetic)
- Already doing arithmetic in cents (integers) and just need to present
- International locale support is important
Always remember:
// DO: Store money as integers in your database
// amount_cents: 1999 (not amount: 19.99)
// DO: Match your payment provider's format
// Stripe uses cents: { amount: 1999, currency: "usd" }
// DON'T: Store money as floats
// amount: 19.99 ← floating point representation issues
// DON'T: Do math with raw floats
// const total = price * 1.08 ← rounding errors accumulate
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on dinero.js v2.x, currency.js v2.x, and ECMAScript Intl specification (supported in all modern browsers and Node.js 12+).