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"), otherwise NaN. The empty string becomes 0, a classic trap for empty form fields. The unary plus, +text, does the same.
  • parseInt(text, 10) and parseFloat(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 to parseInt.
  • String(n), n.toString() and n.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 +∞), so 2.5 becomes 3 and -2.5 becomes −2. Python's round() uses banker's rounding instead (round(2.5) is 2).
  • Math.floor() rounds down, Math.ceil() up, and Math.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.

One Date value, 1790843400000 milliseconds since the epoch, is shown as 08:30 in UTC, 10:30 in Amsterdam, 04:30 in New York and 17:30 in Tokyo on 1 October 2026. 1790843400000 ms since 1970-01-01 UTC UTC 08:30 Amsterdam 10:30 New York 04:30 Tokyo 17:30
A 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 with new 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".