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. toFixedreturns a string and rounds the stored binary value, so it disagrees with the decimal you typed.NaNis not equal to itself — test withNumber.isNaN.Infinity,-0andNaNare alltypeof '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| Task | Reliable approach | Avoid |
|---|---|---|
| Store a timestamp | ISO 8601 UTC string | toLocaleString() output |
| Build a fixed date | new Date(Date.UTC(y, m - 1, d)) | Bare 'yyyy-mm-dd' strings with local intent |
| Add days or months | Convert to ms, or a library for calendar rules | Manual month arithmetic across DST |
| Compare instants | a - b or getTime() | Comparing Date objects with === |
| Validate input | Number.isNaN(date.getTime()) | Truthiness of the object |
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
Intlwhen 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
GBPalongside the amount and format it at the edge.
FAQ
When should I use a date library?
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?
Math, and JSON.stringify throws on it. Use it only where exact large integers matter — database IDs, cryptography, money in minor units at scale.Related
Tooling: linting, formatting, bundling and testing Modules and project structure
Last refreshed 2026-09-18.