Numbers, Math and Dates
Numbers in JavaScript: one floating-point type, why 0.1 + 0.2 is not 0.3, BigInt for very large integers, the Math object, formatting, and the pitfalls of Date and its successor Temporal.
One number type
JavaScript's number type is a
single type for integers and fractions alike: every value is an IEEE
754 double-precision
floating-point number, 64 bits wide. 42 and 42.0 are the same
value, typeof reports "number" for both,
and dividing never truncates. A second numeric type,
BigInt, exists for integers that do not fit.
console.log(typeof 42, typeof 42.5, 42 === 42.0);
console.log(7 / 2); // no integer division
console.log(Math.trunc(7 / 2)); // drop the fraction yourself
console.log(7 % 2, -7 % 2); // remainder keeps the sign of the left side
console.log(2 ** 10); // exponent operator
console.log(1_250_000, 0xff, 0b1010, 0o17, 3.2e3);
number number true 3.5 3 1 -1 1024 1250000 255 10 15 3200
Numeric literals can use _ as a visual separator
(ES2021), 0x for hexadecimal, 0b for
binary and 0o for octal, and e for powers
of ten. All of them produce ordinary numbers.
Floating-point precision
A double stores numbers in binary, and most decimal fractions, such as 0.1, have no exact binary form, just as 1/3 has no exact decimal form. The stored value is the nearest one available, and the tiny differences become visible after arithmetic. The bit layout and the full explanation are on Number Systems; this is a property of the format, so Python, Java and C behave the same.
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON);
// Money: work in whole cents, convert only for display
const tariffCents = 27; // € 0.27 per kWh
const usage = [118, 96, 104.5]; // kWh per week
const totalCents = Math.round(usage.reduce((sum, kwh) => sum + kwh, 0) * tariffCents);
console.log(totalCents, (totalCents / 100).toFixed(2));
0.30000000000000004 false true 8600 86.00
Two habits avoid almost all problems. Compare fractions with a
tolerance (Number.EPSILON is the gap between 1 and the
next double, fine for values near 1; pick a larger tolerance for
larger numbers). And store money as an integer number of cents,
rounding once at a clear point. ES2026 also adds
Math.sumPrecise() for adding many numbers without
accumulating rounding errors; as of 2026 it is in Chrome 147,
Firefox 137 and Safari 26.2 (MDN lists it as Baseline 2026, newly
available since April 2026), but not yet in Node.js.
Safe integers
Integers are exact only up to
Number.MAX_SAFE_INTEGER, which is 253 − 1 =
9,007,199,254,740,991. Above that, not every integer can be stored,
and different integers collapse into the same double. That matters
for database IDs, nanosecond timestamps and amounts in the smallest
currency unit of big systems: values like these are often sent as
strings in JSON for exactly this reason.
console.log(Number.MAX_SAFE_INTEGER);
console.log(9007199254740992 === 9007199254740993);
console.log(Number.isSafeInteger(2 ** 53 - 1), Number.isSafeInteger(2 ** 53));
console.log(JSON.parse('{"id": 1234567890123456789}').id);
9007199254740991 true true false 1234567890123456800
NaN, Infinity and −0
NaN
NaN ("not a number") is a special number
value that marks the result of an operation that has no meaningful
numeric answer, such as converting the text "n/a" to a
number or computing 0 / 0. Despite its name,
typeof NaN is "number". Any arithmetic
with NaN gives NaN, so one bad input can
silently spread through a whole calculation instead of raising an
error.
NaN is the only value that is not equal to itself, so
x === NaN is always false. Use
Number.isNaN(x). The older global
isNaN() first converts its argument to a number, so it
also returns true for any non-numeric string, which is
rarely what you mean.
const readings = ['318.4', '290.1', 'n/a', '305.7'];
const total = readings.reduce((sum, text) => sum + Number(text), 0);
console.log(total); // one bad value spoils the sum
console.log(typeof NaN, NaN === NaN);
console.log(Number.isNaN(total), Number.isNaN('n/a'), isNaN('n/a'));
const valid = readings.map(Number).filter((n) => !Number.isNaN(n));
console.log(valid, valid.length);
NaN number false true false true [ 318.4, 290.1, 305.7 ] 3
Infinity and −0
Dividing by zero does not throw an error in JavaScript: a positive
number divided by 0 is Infinity, a negative one
-Infinity. Numbers that grow beyond about 1.8 × 10308
also become Infinity. A floating-point format also has
a negative zero; it compares equal to 0 with ===, and
you will rarely notice it, except that
Object.is() tells them apart.
Number.isFinite() is the best single check that a value
is a real, usable number: it is false for NaN, both
infinities and non-numbers.
console.log(10 / 0, -10 / 0, 0 / 0);
console.log(Number.MAX_VALUE * 2);
console.log(-0 === 0, Object.is(-0, 0));
console.log(Number.isFinite(42), Number.isFinite(1 / 0), Number.isFinite('42'));
Infinity -Infinity NaN Infinity true false true false false
Converting text to numbers
Input from forms, files, URLs and the command line arrives as text. JavaScript offers several ways to convert it, and they disagree on the edge cases, so choose deliberately:
-
Number(text): the whole string must be a number (spaces around it are allowed, and so are literal forms such as"0x1F"), otherwiseNaN. The empty string becomes0, a classic trap for empty form fields. The unary plus,+text, does the same. -
parseInt(text, 10)andparseFloat(text): read a number from the start of the text and stop at the first character that does not fit, so"12.9 kWh"still gives a number. Always pass the radix 10 toparseInt. -
String(n),n.toString()andn.toString(16)go the other way, optionally in another base.
for (const text of ['42', ' 42 ', '12.9 kWh', '', 'abc', '0x1F']) {
console.log(JSON.stringify(text).padEnd(11),
Number(text), parseInt(text, 10), parseFloat(text));
}
console.log(Number(true), Number(null), Number(undefined));
console.log(String(255), (255).toString(16), (255).toString(2));
"42" 42 42 42 " 42 " 42 42 42 "12.9 kWh" NaN 12 12.9 "" 0 NaN NaN "abc" NaN NaN NaN "0x1F" 31 0 0 1 0 NaN 255 ff 11111111
BigInt for very large integers
BigInt is a separate numeric primitive
type (ES2020) for whole numbers of any size, limited only by memory.
You write one with an n suffix (123n) or
convert with BigInt(). BigInts are exact, so they suit
large IDs, cryptographic values and sums that must not lose a single
unit. Division rounds toward zero, as there are no fractions.
const big = BigInt(Number.MAX_SAFE_INTEGER);
console.log(big + 2n); // exact, unlike the number version
console.log(2n ** 64n);
console.log(7n / 2n, typeof 7n);
console.log(BigInt('1234567890123456789') + 1n);
console.log(10n === 10, 10n == 10, 10n < 11);
9007199254740993n 18446744073709551616n 3n bigint 1234567890123456790n false true true
To prevent silent precision loss, JavaScript refuses to mix BigInt
and ordinary numbers in arithmetic. Convert one side explicitly with
BigInt(n) or Number(big) (the latter may
lose precision). Comparisons such as < and
== do work across the two types, as the last line above
shows.
const meterTotalWh = 9007199254740993n;
console.log(meterTotalWh + 1);
TypeError: Cannot mix BigInt and other types, use explicit conversions
BigInts also cannot be used with Math functions, and
JSON.stringify() throws on them; see
Objects and JSON. Use them where
exact large integers are needed, not as a general replacement for
numbers: they are slower and more awkward.
The Math object
The Math object is a built-in
object that groups mathematical constants and functions:
Math.PI, Math.sqrt(),
Math.max() and so on. It is not a constructor; you
never write new Math(), you call its functions
directly. Four of them turn a fraction into an integer, and they
differ for negative numbers and halves:
for (const x of [2.5, 2.7, -2.5, -2.7]) {
console.log(String(x).padEnd(5),
Math.round(x), Math.floor(x), Math.ceil(x), Math.trunc(x));
}
2.5 3 2 3 2 2.7 3 2 3 2 -2.5 -2 -3 -2 -2 -2.7 -3 -3 -2 -2
-
Math.round()rounds to the nearest integer; an exact half goes up (toward +∞), so2.5becomes 3 and-2.5becomes −2. Python'sround()uses banker's rounding instead (round(2.5)is 2). -
Math.floor()rounds down,Math.ceil()up, andMath.trunc()toward zero (it drops the fraction).
const temperatures = [12.4, 15.1, 9.8, 17.3, 14.0];
console.log(Math.max(...temperatures), Math.min(...temperatures));
console.log(Math.max()); // no arguments: -Infinity
console.log(Math.abs(-4.2), Math.sqrt(2), Math.hypot(3, 4));
console.log(Math.round(Math.PI * 100) / 100);
console.log(Math.sign(-7), Math.cbrt(27), Math.log10(1000));
17.3 9.8 -Infinity 4.2 1.4142135623730951 5 3.14 -1 3 3
Math.max() takes separate arguments, not an array; the
spread syntax ...temperatures unpacks the array into
arguments (see Arrays).
Random numbers
Math.random() returns a pseudo-random number from 0
(included) to 1 (excluded). To get a whole number in a range, scale
and floor it. Its output is not suitable for passwords,
tokens or anything security-related, because it is predictable; use
crypto.getRandomValues() or
crypto.randomUUID(), available in browsers and Node.js,
for that. The output changes on every run, so this block is not run
here:
// A whole number from min to max, both included
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(randomInt(1, 6)); // a die roll: 1, 2, 3, 4, 5 or 6
// For security purposes, use the Web Crypto API instead
console.log(crypto.randomUUID()); // e.g. '3b241101-e2bb-4255-8caf-4136c566a962'
Formatting numbers for people
Printing a raw number often shows too many decimals or an unusual format. Two built-in tools cover the common cases:
-
n.toFixed(digits)returns a string with a fixed number of decimals. It works on the stored binary value, so some halves round down:(1.005).toFixed(2)is"1.00", because 1.005 is stored as slightly less. Fine for display, not for financial rounding rules. -
n.toPrecision(digits)rounds to a number of significant digits.
The Intl API
The Intl API (the
Intl object, from the separate ECMA-402
internationalization standard) formats numbers, currencies,
percentages, dates, lists and plural forms according to a
locale, a code such as en-US or
nl-NL that names a language and region. Use it for
anything a person reads: it knows that the Netherlands writes
1.234,50 where the United States writes
1,234.50, and where the currency sign goes.
n.toLocaleString(locale, options) is a shortcut to the
same machinery.
const amount = 1234.5;
const eurUS = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' });
const eurNL = new Intl.NumberFormat('nl-NL', { style: 'currency', currency: 'EUR' });
console.log(amount.toFixed(2), (1.005).toFixed(2), (2.345).toFixed(2));
console.log(eurUS.format(amount));
console.log(eurNL.format(amount).replace(' ', ' '));
console.log(new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 1 }).format(0.0753));
console.log(new Intl.NumberFormat('en', { notation: 'compact' }).format(2150000));
console.log((1234567.891).toLocaleString('en-US', { maximumFractionDigits: 1 }));
1234.50 1.00 2.35 €1,234.50 € 1.234,50 7.5% 2.2M 1,234,567.9
Some locales put a non-breaking space (U+00A0) between the currency
sign and the amount; the example replaces it with an ordinary space
only to keep the printed output plain. Without a locale argument,
Intl uses the default locale of the browser or system,
so the same code can print differently on another machine. Pass the
locale explicitly when the format matters, for example in invoices
or exports. Never parse a formatted string back into a number; keep
the number.
Dates and times with Date
A Date object represents one
instant in time, stored as the number of milliseconds since the Unix
epoch, 1 January 1970 00:00:00 UTC (Coordinated Universal Time). It
has no time zone of its own: methods such as
getHours() show that instant in the local time
zone of the machine, and methods with UTC in their
name, such as getUTCHours(), show it in UTC. Date is
from the very first version of JavaScript (1995), modeled on Java's
java.util.Date class, including some of its flaws, and
has well-known design problems.
Date is one number. The local time you see depends
on the time zone used to display it.
Creating dates
The safest ways to create a date are from an ISO 8601 string with an
explicit offset ("2026-10-01T08:30:00Z", where
Z means UTC) or from parts with
Date.UTC(). toISOString() always prints
the instant in UTC, in the
ISO 8601
format used in JSON and APIs.
const outageStart = new Date('2026-10-01T08:30:00Z');
const sameInstant = new Date(Date.UTC(2026, 9, 1, 8, 30)); // month 9 = October
console.log(outageStart.toISOString());
console.log(outageStart.getTime(), sameInstant.getTime() === outageStart.getTime());
console.log(outageStart.getUTCFullYear(), outageStart.getUTCMonth(), outageStart.getUTCDate());
console.log(outageStart.getUTCDay()); // 0 = Sunday ... 4 = Thursday
console.log(new Date(0).toISOString());
2026-10-01T08:30:00.000Z 1790843400000 true 2026 9 1 4 1970-01-01T00:00:00.000Z
The classic Date pitfalls
-
Months start at 0. January is 0 and December is
11, while days of the month start at 1.
new Date(2026, 10, 1)is 1 November. - Overflow rolls over silently. 30 February becomes 2 March instead of an error, so invalid input dates are not caught.
-
Dates are mutable. Setters such as
setUTCDate()change the object itself, so a function that "adds a day" to a date it received changes the caller's date too. Copy first withnew Date(date). -
Parsing depends on the format. A date-only ISO
string (
"2026-10-01") is read as UTC midnight, but a date-time without an offset ("2026-10-01T00:00") is read as local time. Other formats, such as"10/01/2026", are not covered by the standard and may differ between engines; avoid them.
console.log(new Date(Date.UTC(2026, 1, 30)).toISOString()); // 30 February?
const due = new Date('2026-01-31T00:00:00Z');
const reminder = due; // same object, not a copy
reminder.setUTCMonth(reminder.getUTCMonth() + 1);
console.log(due.toISOString()); // changed too, and "31 February" rolled over
const original = new Date('2026-01-31T00:00:00Z');
const copy = new Date(original); // a real copy
copy.setUTCDate(copy.getUTCDate() + 7);
console.log(original.toISOString(), copy.toISOString());
2026-03-02T00:00:00.000Z 2026-03-03T00:00:00.000Z 2026-01-31T00:00:00.000Z 2026-02-07T00:00:00.000Z
The parsing difference only shows on a machine that is not set to UTC. In a browser in the Netherlands, in summer time:
new Date('2026-10-01').toString();
new Date('2026-10-01T00:00').toString();
'Thu Oct 01 2026 02:00:00 GMT+0200 (Central European Summer Time)' 'Thu Oct 01 2026 00:00:00 GMT+0200 (Central European Summer Time)'
Date arithmetic and time zones
Subtracting two dates gives the difference in milliseconds. To show
a date to a person, use
Intl.DateTimeFormat (or toLocaleString())
with an explicit timeZone, an IANA (Internet Assigned
Numbers Authority) zone name such as Europe/Amsterdam.
The example below also shows why "add 24 hours" and "add one day"
are not the same: on 29 March 2026 the Netherlands moved its clocks
forward, so that day had only 23 hours.
const start = new Date('2026-10-01T08:30:00Z');
const restored = new Date('2026-10-01T10:05:00Z');
const minutes = (restored - start) / 60_000;
console.log(`Outage lasted ${minutes} minutes`);
const nl = new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium', timeStyle: 'short', timeZone: 'Europe/Amsterdam',
});
console.log(nl.format(start));
// Daylight saving time: clocks jump from 02:00 to 03:00 on 29 March 2026
const beforeSwitch = new Date('2026-03-29T00:30:00Z');
const oneHourLater = new Date(beforeSwitch.getTime() + 60 * 60 * 1000);
console.log(nl.format(beforeSwitch), '->', nl.format(oneHourLater));
Outage lasted 95 minutes 1 Oct 2026, 10:30 29 Mar 2026, 01:30 -> 29 Mar 2026, 03:30
Temporal: the successor to Date
Temporal is a new built-in API for
dates and times, designed to replace Date. It has
separate immutable types for different needs: a calendar date
without a time (PlainDate), a wall-clock time
(PlainTime), an exact instant (Instant)
and a date-time in a named time zone (ZonedDateTime).
Months are numbered 1 to 12, every operation returns a new value,
and arithmetic is time-zone and daylight-saving aware.
As of October 2026, Temporal has reached Stage 4 in TC39, the
committee that develops the language, and is expected in ECMAScript
2027; it is not part of ES2026. It ships in Firefox 139+,
Chrome and Edge 144+ and Node.js 26+; Safari has it only in its
Technology Preview. For other environments, polyfills such as
@js-temporal/polyfill exist. The block below is not run
here, because the checker's Node.js 24 does not have Temporal.
// Node.js 26+, Firefox 139+, Chrome/Edge 144+ (not Node 20, 22 or 24)
const due = Temporal.PlainDate.from('2026-01-31');
console.log(due.add({ months: 1 }).toString()); // '2026-02-28' (clamped, no rollover)
console.log(due.month); // 1 = January
const start = Temporal.PlainDate.from('2026-10-01');
console.log(start.until('2026-12-25').days); // 85
const meeting = Temporal.ZonedDateTime.from('2026-03-28T09:00[Europe/Amsterdam]');
console.log(meeting.add({ days: 1 }).toString());
// '2026-03-29T09:00:00+02:00[Europe/Amsterdam]': same wall-clock time, new offset
Until Temporal is available everywhere your code runs, the practical
advice is: use Date only as a timestamp, do
calculations in UTC, format with Intl, and reach for a
well-maintained date library or the Temporal polyfill when you need
calendar arithmetic such as "add one month".