A numeric literal is the way you write a number directly in your source code. In JavaScript that covers ordinary decimal numbers, numbers written in other bases (binary, octal, hexadecimal), and BigInt integers. JavaScript’s parser turns these character sequences into actual numeric values as part of the lexical grammar, long before your code runs. Critically, numeric literals themselves are always unsigned; a leading -
or +
is a separate unary operator applied to the literal afterward.
// Unsigned literal + unary operator
-123.45 // parsed as unary minus applied to the literal 123.45
+0xFF // parsed as unary plus applied to the literal 0xFF
Decimal literals
Write base-10 numbers with digits 0–9
. You can include a fractional part and an exponent for scientific notation.
42
3.14159
0.5
5e3 // 5000
1.25e-3 // 0.00125
Exponents use e
or E
followed by an optional sign and at least one digit. The decimal point is optional if you use an exponent, but you can’t have two decimal points and you can’t put an exponent on a BigInt (we’ll get there soon).
0.25e2 // 25
2.E3 // 2000 (fractional part omitted)
.5e1 // 5 (leading 0 omitted)
Integers in other bases: binary, octal, and hexadecimal
JavaScript supports integer literals in bases 2, 8, and 16 using standard prefixes:
0b1010 // binary 10 (decimal 10)
0o755 // octal 755 (decimal 493)
0xFF_A9 // hex FF A9 (decimal 65449)
Binary uses 0b
/0B
with only 0
and 1
. Octal uses 0o
/0O
with digits 0–7
. Hex uses 0x
/0X
and digits 0–9
plus A–F
/a–f
. These syntaxes work in both strict and non-strict code.
There’s also a historical, legacy octal form that uses a leading 0
like 0755
. That form is forbidden in strict mode and should be avoided entirely in modern code. Stick to 0o755
.
"use strict";
// 0755; // SyntaxError in strict mode — legacy octal
0o755; // Preferred and valid everywhere
When you see a leading 0
without x
, o
, or b
after it and any digit greater than 7
appears, the literal is treated as decimal (or a syntax error in strict mode for some patterns). Avoid ambiguity by not using padded integers like 0005
; write 5
instead.
BigInt literals
BigInt represents whole numbers of arbitrary size. Append n
to an integer literal to produce a BigInt value. You can use decimal, binary, octal, or hex forms—still integers only.
9007199254740993n // larger than Number.MAX_SAFE_INTEGER
0b1_0000_0000_0000_0000n // 65536n in binary
0o7_000_000_000_000n // octal BigInt
0x1f_ff_ff_ff_ff_ffn // hex BigInt
BigInt literals can’t have a decimal point or an exponent, and you can’t mix them directly with Number
in arithmetic without explicit conversion. Use BigInt()
for intentional conversion from integers; fractional numbers can’t be converted.
1.5n // ❌ SyntaxError: BigInt with decimal
1e3n // ❌ SyntaxError: BigInt with exponent
BigInt(42) // 42n
BigInt(42.1) // RangeError (not an integer)
Numeric separators (underscores) for readability
You can insert underscores between digits to group them for readability in any numeric base and with BigInt. They don’t change the value; they’re ignored by the parser.
1_000_000 // one million
3.141_592_653_589_793 // grouped fractional part
0b1101_0101_1001_0001 // binary grouping
0xFF_FF_00_00 // hex grouping
1_000_000_000_000_000n // BigInt with separators
A few rules keep things unambiguous. You can’t put an underscore at the start or end of the number, you can’t have two in a row, and you can’t put one right next to the decimal point, right next to the exponent marker e
/E
or its sign, or in the base prefix.
100__000 // ❌ two in a row
_1000 // ❌ starts with underscore
1000_ // ❌ ends with underscore
1_.5 // ❌ next to decimal point
1._5 // ❌ next to decimal point
1e_3 // ❌ next to exponent marker
1e+_3 // ❌ next to exponent sign
0x_FF // ❌ right after the prefix
0b1010_ // ❌ ends with underscore
If you need a human-friendly string version of a number with separators, format it yourself (toLocaleString
, custom formatter, etc.). Parsers like Number()
do not accept underscores inside strings.
Number("1_000") // NaN — underscores are only for *literals*, not strings
Signed numbers are expressions, not literals
You often write -8
or +3.5
, but the sign isn’t part of the literal. It’s a separate unary operator applied to the literal that follows. This matters in a few places such as grammar edge cases and minified code. If you’re tokenizing or doing static analysis, treat -
and +
as operators, not as part of the numeric token.
const a = -0xFF; // unary minus + hex literal
const b = +1_000; // unary plus + decimal literal with separators
Infinity and NaN aren’t numeric literals
Infinity
and NaN
are identifiers bound to special numeric values, not literal syntax. Use them when that’s what you mean, but don’t confuse them with number tokens.
Infinity // a global property whose value is a Number
NaN // “not a number” — also a Number value
Putting it all together with examples
Here are practical patterns that come up frequently in day-to-day code. Each line shows a literal you’d actually write and the value JavaScript produces.
// Decimal, fractional, and exponential forms
const rate = 0.075; // 0.075
const nano = 1e-9; // 0.000000001
const bigExp = 12.5e6; // 12500000
// Other bases
const mask = 0b1111_0000; // 240
const mode = 0o644; // 420
const color = 0xFF_FF_00; // 16776960
// BigInt variants
const files = 1_000_000_000_000n;
const flags = 0b1_0101n;
const guid = 0xDEAD_BEEF_F00Dn;
// Signed forms are expressions
const neg = -0b1010; // -10
const pos = +.5e1; // 5
// Separator pitfalls (don’t do these)
// 1_e3; // ❌
/// e1_0; // ❌
Interop tips and pitfalls you’ll actually hit
If you paste data with leading zeros into source files, remember that 0755
is legacy octal and will break in strict mode. Always spell octal as 0o755
. If you rely on string parsing, don’t expect underscores to work — they’re only valid when the parser sees a literal in code. And finally, don’t try to mix Number
and BigInt
arithmetic without converting on purpose; JavaScript will throw to prevent silent precision bugs.
// Safe octal
const perm = 0o755;
// String parsing doesn’t allow separators
parseInt("1_000", 10); // 1, because it stops at '_' (not what you want)
Number("1_000"); // NaN
// Number ↔ BigInt requires intent
10n + 5; // ❌ TypeError
10n + BigInt(5); // ✅ 15n
Number(10n) + 5; // ✅ 15
How JavaScript decides what your digits “mean”
Under the hood, the ECMAScript lexical grammar defines how sequences of characters become numeric tokens and then “mathematical values.” That grammar is why, for example, 1_000
is allowed but 1__000
is not, why 0o
changes the valid digit set, and why the -
in -2
isn’t part of the numeric token. You don’t need to memorize the production rules, but it’s useful to know that these behaviors are specified precisely and uniformly across engines.
Quick sanity checks you can keep in your tests
If you want to ensure you’re using literals safely in a codebase, add a few focused tests and lint rules: numbers using separators still equal their separator-free forms, BigInt values never appear with decimals or exponents, and all octal values use 0o
rather than a leading 0
. ESLint rules like no-loss-of-precision
and TypeScript’s checks will catch many of the pitfalls, but unit tests that exercise real literals are a great backstop.
// Example test snippets (Jest)
expect(1_000_000).toBe(1000000);
expect(0o755).toBe(493);
expect(() => eval("1e3n")).toThrow(); // BigInt exponent not allowed
expect(() => eval("'use strict'; 0755")).toThrow(); // legacy octal in strict