Skip to main content

Guide

dinero.js vs currency.js vs Intl.NumberFormat 2026

Compare dinero.js, currency.js, and Intl.NumberFormat for handling money in JavaScript. Floating point issues, currency formatting, arithmetic precision.

·PkgPulse Team·
0

TL;DR

Never use floating-point arithmetic for money0.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.30000000000000004 in 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

Featuredinero.jscurrency.jsIntl.NumberFormat
Integer arithmetic❌ (display only)
Multi-currency✅ (formatting only)
Rounding strategies✅ Multiple
Chainable API❌ (functional)
Currency formattingVia Intl
Bundle size~9KB~1KBBuilt-in (0KB)
TypeScript
ESMN/A
Locale-awareVia 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

Database Storage Strategies for Monetary Values

How you store money in a database directly affects which library you use and what precision you can achieve. The universal recommendation is to store amounts as integers (cents or the smallest currency subunit) in a regular integer or bigint column — never as a float or decimal. PostgreSQL's NUMERIC type and DECIMAL are precise but slower for arithmetic than integers. Stripe's API stores amounts in cents as integers and sends them back as integers — match this representation in your database to avoid any conversion. When storing multi-currency amounts, the common pattern is two columns: amount_cents INTEGER and currency_code VARCHAR(3). dinero.js's toSnapshot() function serializes a Dinero value to a plain object with exactly these two fields, making database persistence straightforward. When restoring from the database, dinero({ amount: row.amount_cents, currency: CURRENCIES[row.currency_code] }) reconstructs the Dinero value correctly.

TypeScript Integration and Generic Money Types

Both dinero.js v2 and currency.js have TypeScript support, but their type signatures reflect their different philosophies. dinero.js uses generic types to encode currency information: Dinero<number> where the type parameter represents the amount type. This enables multi-precision arithmetic where different currencies can have different numbers of decimal places (JPY has 0, USD has 2, KWD has 3) without runtime errors. TypeScript catches you if you try to add a USD dinero value to a JPY dinero value — though in practice you'll write the type guard logic rather than relying on the type system to prevent this. currency.js uses simpler types since it doesn't model currency identity in the type system — it's Currency regardless of which currency you're representing. For multi-currency applications, dinero.js's explicit type modeling helps catch logic errors; for single-currency apps, currency.js's simplicity wins.

Rounding and Financial Regulation

Rounding strategy is not just a technical preference — in many jurisdictions it is legally mandated. Banking regulations in the EU, UK, and US specify which rounding method must be used for interest calculations, tax computations, and fee splitting. Banker's rounding (round half to even) is the standard for financial calculations because it eliminates the systematic upward bias introduced by "round half up." dinero.js provides all common rounding strategies (up, down, halfUp, halfDown, halfEven) and makes the choice explicit in the divide() call. currency.js defaults to round-half-up with two decimal places — appropriate for display but potentially non-compliant for regulated financial calculations. When building payment processing, tax calculation, or accounting software, consult the applicable regulatory requirements and choose your rounding strategy to match.

Internationalization Beyond Currency Symbols

Intl.NumberFormat does much more than attach a currency symbol to a number. The full locale string determines thousands separators (, in US/UK, . in Germany/France, ' in Switzerland), decimal separators, number grouping patterns, the position of the currency symbol (before or after), and whether negative amounts use parentheses or a minus sign. For a global application that needs to display monetary values correctly for users in different countries, Intl.NumberFormat with the user's locale string from the browser or Accept-Language header produces correctly formatted output without any manual locale logic. Performance tip: Intl.NumberFormat instantiation is expensive — create one formatter instance per locale-currency combination and cache it rather than instantiating on every render. In a React application, memoize the formatter with useMemo or create it at module scope for fixed locale requirements.

Migrating from Float-Based Legacy Code

Migrating existing float-based money calculations to integer cents is one of the most impactful refactors for financial software correctness. The migration strategy depends on your data layer. For PostgreSQL with float columns: add a new amount_cents integer column, run a migration that converts existing float values by multiplying by 100 and rounding (ROUND(amount * 100)::INTEGER), verify the converted values match expectations, then update application code to use the new column and drop the old one. The multiplying step has a subtle risk: if the stored float values already have rounding errors (e.g., 19.990000000000001 stored as a float), the multiplication will produce 1999.0000000000002 which rounds correctly to 1999. In practice this works, but you should audit a sample of conversions before committing to the migration in production.

Testing Money Calculations

Financial calculations require property-based testing beyond simple unit tests. A property that must hold for any monetary arithmetic: adding then subtracting the same amount should return the original value. Dividing and multiplying should approximately recover the original (within rounding tolerance for odd divisions). These properties catch rounding implementation bugs that specific example-based tests miss. The fast-check library provides property-based testing for TypeScript and works well for generating random monetary amounts across ranges. For currency formatting tests, test with edge cases: zero amounts, maximum safe integer values, negative amounts, zero-decimal currencies like JPY, three-decimal currencies like KWD. Snapshot tests for formatted output are fragile across Node.js versions since Intl.NumberFormat behavior can change — prefer asserting on structural properties (contains currency symbol, digits are correct) rather than exact string matches.

Multi-Currency Architecture Decisions

Applications that handle multiple currencies simultaneously require clear architectural decisions about when and how currency conversion happens. The general principle is to store all monetary values in their original currency and convert only at display time using current exchange rates — never convert and store at a single historical rate. This preserves the original transaction value and lets you apply updated exchange rates retrospectively for reporting. When comparing amounts across currencies for analytics (total revenue in USD equivalent), fetch current or period-average exchange rates from an FX API and apply them in the reporting layer. dinero.js provides the currency-aware arithmetic but explicitly does not include FX rate conversion — you supply the rate from your chosen FX data source. For applications that accept payment in one currency and pay out in another, store both the input amount in original currency and the converted amount with the exchange rate used, creating an audit trail for financial reconciliation.

Compare financial and utility packages on PkgPulse →

See also: AVA vs Jest and tsoa vs swagger-jsdoc vs Zodios, change-case vs camelcase vs slugify.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.