Numbers, dates and internationalisation

Floating-point precision, BigInt, the Math helpers worth knowing, Date pitfalls with UTC, and Intl formatting for numbers, dates and relative time.

Precision, BigInt and Math

All ordinary numbers are IEEE 754 doubles. That is exact for integers up to 2^53-1 and approximate for most decimals, which is why currency must be handled in whole minor units rather than in pounds and pence as floats.

0.1 + 0.2;                    // 0.30000000000000004
0.1 + 0.2 === 0.3;            // false
Number.EPSILON;               // 2.220446049250313e-16
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON;   // true - the right comparison

Number.isInteger(4.0);        // true
Number.isSafeInteger(2 ** 53); // false - beyond exact integer range
(1.005).toFixed(2);           // '1.00' - rounds on the binary value, not the decimal

// work in minor units instead
const total = 1999 + 250;     // pence
(total / 100).toFixed(2);     // '22.49'

// BigInt: exact integers of any size
const big = 9007199254740993n;
big * 2n;                     // 18014398509481986n
Number(big);                  // lossy - do not mix casually
typeof big;                   // 'bigint' - a distinct type
big + 1;                      // TypeError: cannot mix BigInt and Number

// Math helpers that earn their place
Math.round(2.5);              // 3
Math.trunc(-2.7);             // -2   (Math.floor would give -3)
Math.min(...values);
Math.max(...values);
Math.hypot(3, 4);             // 5
Math.clamp?.(0, 1);           // feature-detect newer helpers
(7).toString(2);              // '111' - binary string
parseInt('ff', 16);           // 255
Number('12px');               // NaN - use parseFloat for a lenient parse
parseFloat('12px');           // 12
  • Never compare floats with ===; compare the difference against an epsilon, or keep values as integers.
  • toFixed returns a string and rounds the stored binary value, so it disagrees with the decimal you typed.
  • NaN is not equal to itself — test with Number.isNaN.
  • Infinity, -0 and NaN are all typeof 'number'; validate input rather than trusting the type.

Dates and time zones

A Date is a single instant in time with no time zone of its own. Only the getters and setters that carry a local or UTC prefix decide how that instant is displayed, and month numbers start at zero.

const now = new Date();
Date.now();                              // milliseconds since the epoch

new Date('2026-03-01');                  // parsed as UTC midnight
new Date('2026-03-01T09:30:00Z');        // explicit UTC
new Date('2026-03-01T09:30:00');         // local - differs per machine
new Date(2026, 2, 1);                    // local, month index 2 === March

const d = new Date(Date.UTC(2026, 2, 1, 9, 30));
d.getUTCHours();                         // 9
d.getHours();                            // depends on the machine's zone
d.toISOString();                         // '2026-03-01T09:30:00.000Z'
d.toLocaleDateString('en-GB');           // '01/03/2026'

// arithmetic is in milliseconds
const tomorrow = new Date(d.getTime() + 24 * 60 * 60 * 1000);

// differences
const days = Math.round((a - b) / 86400000);

// invalid dates fail silently
new Date('not a date').getTime();        // NaN
Number.isNaN(new Date('x').getTime());   // the check you need

// store UTC, display local, keep the zone of the user separate from the zone of the data
TaskReliable approachAvoid
Store a timestampISO 8601 UTC stringtoLocaleString() output
Build a fixed datenew Date(Date.UTC(y, m - 1, d))Bare 'yyyy-mm-dd' strings with local intent
Add days or monthsConvert to ms, or a library for calendar rulesManual month arithmetic across DST
Compare instantsa - b or getTime()Comparing Date objects with ===
Validate inputNumber.isNaN(date.getTime())Truthiness of the object
⚠️
Adding 24 hours of milliseconds does not always add one calendar day: across a daylight-saving change the local time shifts by an hour. For calendar maths (billing periods, deadlines) use UTC arithmetic or a date library, and never compare a local date string with a UTC one.

Intl formatting

The Intl API formats numbers, currency, dates and relative time for a locale without shipping a formatting library. Construct the formatter once and reuse it — building one per call is the common performance mistake.

const gbp = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' });
gbp.format(1234.5);                      // '£1,234.50'

const compact = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 });
compact.format(12800);                   // '12.8K'

const pct = new Intl.NumberFormat('en', { style: 'percent', maximumFractionDigits: 1 });
pct.format(0.1234);                      // '12.3%'

const date = new Intl.DateTimeFormat('de-DE', {
  dateStyle: 'medium', timeStyle: 'short', timeZone: 'Europe/Berlin',
});
date.format(new Date(Date.UTC(2026, 2, 1, 9, 30)));   // '01.03.2026, 10:30'

const rel = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rel.format(-1, 'day');                   // 'yesterday'
rel.format(3, 'hour');                   // 'in 3 hours'

const list = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
list.format(['a', 'b', 'c']);            // 'a, b and c'

// discover what the user's environment actually prefers
Intl.DateTimeFormat().resolvedOptions().timeZone;
new Intl.NumberFormat().resolvedOptions().locale;
  • Pass an explicit locale instead of relying on the environment when output must be stable in tests and on the server.
  • Bit integers beyond the safe range only format correctly through Intl when you pass a BigInt or a string.
  • For relative time, compute the difference yourself and pass it in seconds, minutes or days — the formatter does not compare dates.
  • Currency codes are data, not presentation: store GBP alongside the amount and format it at the edge.

FAQ

When should I use a date library?
When you need calendar arithmetic, time-zone conversion by name, or parsing of many human formats. The built-in Date plus Intl covers storing, displaying and comparing instants, which is most of what an application does.
Is BigInt a drop-in replacement for number?
No. It cannot mix with Number in arithmetic, it is not accepted by Math, and JSON.stringify throws on it. Use it only where exact large integers matter — database IDs, cryptography, money in minor units at scale.

Tooling: linting, formatting, bundling and testing Modules and project structure

Last refreshed 2026-09-18.