JavaScript 71 🧬 Numeric separators
Large numbers are hard to read. 1000000000 and 100000000 look almost identical at a glance — one is a billion, the other is a hundred million, and the human eye can’t reliably tell them apart. Numeric separators fix this: an underscore you place wherever you want, that JavaScript ignores entirely.
The feature is small. It doesn’t change behavior, add runtime cost, or affect any computation. It’s purely a readability feature — but for anyone who works with money, IDs, byte sizes, timestamps, or cryptographic values, it pays for itself immediately.
Key point: The underscore in 1_000_000 is not part of the number. It’s stripped during parsing. 1_000_000 === 1000000 is true. The separator exists only in the source code.
a – What are numeric separators
A numeric separator is an underscore (_) placed between digits of a numeric literal. JavaScript ignores it completely. You can use it on integers, floats, hex, binary, octal, and BigInt.
Integers:
const million = 1_000_000;
const billion = 1_000_000_000;
const trillion = 1_000_000_000_000;
console.log(million === 1000000);
Each of these is identical to the version without underscores. The separators help you count zeros without squinting.
Floats:
const pi = 3.141_592_653;
const micro = 0.000_001;
const bigFloat = 1_234_567.89;
You can put separators in the integer part, the decimal part, or both. The rules are the same — a separator must sit between two digits.
Hex, binary, octal:
const hexColor = 0xFF_FF_FF;
const mask = 0b1111_0000_1010_0101;
const perm = 0o755_644;
For hex and binary, grouping by 4 (nibble) or 8 is common. For octal, it’s less standardized.
BigInt:
const huge = 9_007_199_254_740_991n;
const id = 1_234_567_890_123_456_789n;
BigInt literals support separators too — critical since they’re often very long.
Exponential notation:
const avogadro = 6.022_140_76e23;
const charge = 1.602_176_634e-19;
The e notation works with separators in the mantissa.
The rule — separators go between digits:
1_000 // ✅
1__000 // ❌ multiple underscores in a row
_1000 // ❌ leading underscore
1000_ // ❌ trailing underscore
1_000.000_1 // ✅
1_.000 // ❌ adjacent to dot
0x_FF // ❌ adjacent to prefix
Only one underscore between two digits. No leading, no trailing, no adjacency to ., x, b, o, e, or the n suffix.
Where separators are ignored:
Every numeric literal position:
| Literal | With separator |
|---|---|
| Integer | 1_000 |
| Float | 1_000.5 |
| Hex | 0xFF_FF |
| Binary | 0b1010_1010 |
| Octal | 0o7_5_5 |
| BigInt | 1_000n |
| Exponent | 1_000e3 |
A note on grouping:
There’s no rule about where to put separators — you choose. Common conventions:
| Domain | Grouping | Example |
|---|---|---|
| General | 3 (thousands) | 1_000_000 |
| Hex | 4 or 8 | 0xFF_FF_FF_FF |
| Binary | 4 or 8 | 0b1111_0000 |
| Credit cards | 4 | 4_242_4242_4242_4242 |
| Phone numbers | 3-4 | 555_123_4567 |
| Timestamps | 3 | 1_705_314_600 |
The separator is yours to place — pick the grouping that matches the domain.
Why numeric separators matter:
- Readability — count zeros at a glance
- Correctness — reduce off-by-a-power-of-ten bugs
- Domain fit — group by credit card chunks, byte sizes, phone patterns
- Zero cost — no runtime impact, no bundle size change
When to use them:
- Large integers (
1_000_000) - Long decimal values (
3.141_592) - Hex color codes, masks (
0xFF_FF_FF) - Binary bit patterns (
0b1010_0101) - BigInt literals (
9_007_199_254_740_991n) - Financial amounts (
1_234_567.89) - Byte counts (
1_073_741_824for 1 GiB)
When not to bother:
- Small numbers (
42,100) - Numbers where digits have no natural grouping (like version numbers
1.2.3— those aren’t numbers, they’re strings) - Anywhere they hurt more than help
b – Rules, edge cases, and gotchas
The feature is simple, but a few rules will bite you if you don’t know them.
Rule 1 — Between digits only:
1_000 // ✅
1_0_0_0 // ✅ — legal but silly
1__0 // ❌ consecutive underscores
_1_000 // ❌ leading
1_000_ // ❌ trailing
A separator must have a digit on both sides.
Rule 2 — Not adjacent to prefixes or suffixes:
0x_FF // ❌ after 0x
0xFF_ // ❌ trailing
0b_1010 // ❌ after 0b
0o_755 // ❌ after 0o
1_000n // ✅ before n
1000_n // ❌ before n
The n suffix for BigInt sits after the last digit; a separator can’t be immediately before it.
Rule 3 — Not adjacent to the decimal point:
1_000.000_1 // ✅
1_.000 // ❌
1._000 // ❌
1_000_.5 // ❌
The dot is not a digit. A separator must be between two digits — the dot breaks that.
Rule 4 — Not adjacent to e:
1_000e3 // ✅
1e_3 // ❌
1_e3 // ❌
The exponent marker is not a digit either.
Rule 5 — Underscores don’t exist at runtime:
const a = 1_000;
const b = 1000;
console.log(a === b); // true
console.log(Object.is(a, b)); // true
console.log(typeof a); // 'number'
The separator is purely lexical. After parsing, it’s gone.
Rule 6 — Number() doesn’t accept separators:
Number('1_000'); // NaN
parseInt('1_000'); // 1
The separator works only in source code literals, not in strings parsed at runtime. Number('1_000') returns NaN because _ isn’t a valid character in a numeric string.
This is a common surprise — the feature is a syntax feature, not a parsing feature.
Rule 7 — JSON doesn’t allow separators:
{ "price": 1_000 }
That’s invalid JSON. Numeric separators are a JavaScript feature, not a JSON one. When serializing, the output has no underscores:
JSON.stringify({ price: 1_000 });
// '{"price":1000}'
Rule 8 — Works in all number literal forms:
1_000 // ✅ integer
1_000.5 // ✅ float
0xFF_FF // ✅ hex
0b1010_1010 // ✅ binary
0o7_5_5 // ✅ octal
1_000n // ✅ BigInt
1.5e1_0 // ✅ exponent
The rules are uniform across all literal forms.
Rule 9 — No impact on operations:
const a = 1_000_000;
const b = 1_000;
const c = a / b; // 1000
console.log(c); // 1000
console.log(c === 1000); // true
Arithmetic ignores the separators completely.
Rule 10 — Grouping is a convention, not a rule:
You can group by 2, 3, 4 — whatever fits your domain. JavaScript doesn’t care.
const ip = 192_168_1_1; // not actually an IP, but readable
const cc = 4_242_4242_4242_4242; // grouped by 4
const bin = 0b1010_0101_1111_0000; // grouped by 4
The error cases:
If you break a rule, you get a SyntaxError at parse time. The message usually says “Invalid or unexpected token” or “Identifier directly after number.”
const bad = 1__000;
// SyntaxError: Numeric separator can not be used after leading 0
The parser is strict — no separators where they don’t belong.
When to use separators — practical guidance:
| Domain | Convention |
|---|---|
| Money | Group by 3 (thousands) |
| Hex colors | Group by 2 (0xFF_00_AA) |
| Binary flags | Group by 4 or 8 |
| IDs | Group by 3 |
| Byte sizes | Group by 3 (1_073_741_824) |
| Credit cards | Group by 4 |
| Phone numbers | Group by 3-4 |
| Timestamps | Group by 3 |
The grouping should match how a human reads the number in that domain.
When not to use separators:
- When the number is short (
42,100,1000) - When the grouping doesn’t add meaning
- When the number represents something with a natural unit (like
2024for a year — a separator would be noise) - In serialized output — never emit separators in JSON, CSV, or URLs
Style guide adoption:
Most teams adopt a simple rule: use separators for any number with 5+ digits. Small numbers stay bare.
10 // no separator
1000 // no separator
10_000 // separator
1_000_000 // separator
Some teams extend to hex and binary always. Others reserve it for very large numbers. There’s no universal rule — pick one and stay consistent.
c – Domain-specific uses
The separator is a readability tool. These are the domains where it earns its keep.
Money and finance:
const accountBalance = 1_234_567.89;
const transactionFee = 0.000_001;
const marketCap = 2_500_000_000_000;
In financial code, a missing zero is a lost decimal place. Grouping by thousands catches errors at a glance.
Byte sizes:
const KB = 1_024;
const MB = 1_048_576;
const GB = 1_073_741_824;
const TB = 1_099_511_627_776;
These are the exact values — 1 KiB is 1024 bytes, not 1000. The separators make it obvious at a glance which is which.
Unix timestamps:
const JAN_1_2024 = 1_704_067_200;
const JAN_1_2025 = 1_735_689_600;
Timestamps are 10 digits and easy to mistype. Grouping by 3 makes them readable and comparable.
Milliseconds:
const SECOND = 1_000;
const MINUTE = 60_000;
const HOUR = 3_600_000;
const DAY = 86_400_000;
Millisecond constants are common in timers, timeouts, and date math.
BigInt for snowflake IDs:
const userId = 1_234_567_890_123_456_789n;
const messageId = 9_876_543_210_987_654_321n;
Snowflake IDs (Twitter, Discord, etc.) are 64-bit integers — they don’t fit in Number, so they’re BigInt, and they’re long. Separators are essential.
Hex colors:
const red = 0xFF_00_00;
const green = 0x00_FF_00;
const blue = 0x00_00_FF;
const white = 0xFF_FF_FF;
const black = 0x00_00_00;
Each byte is a channel. Grouping by 2 makes the RGB components obvious.
Binary bitmasks:
const READ = 0b0001;
const WRITE = 0b0010;
const EXEC = 0b0100;
const ALL = 0b0111;
const flags = 0b1111_0000_1010_0101;
Grouping by 4 or 8 lets you see bit positions at a glance.
Phone numbers (as identifiers):
const supportLine = 1_800_555_0199;
const emergency = 911;
Phone numbers aren’t arithmetic — but when they’re stored as numbers (for lookup tables), separators group them by area code.
Credit card test numbers:
const testVisa = 4_242_4242_4242_4242;
const testMastercard = 5_555_5555_5555_4444;
Grouped by 4 — same as printed on the card. Note: in production, card numbers should be strings, not numbers, because of leading zeros and length.
Geographic coordinates:
const latitude = 37.774_929;
const longitude = -122.419_416;
The decimal part benefits from grouping.
Scientific constants:
const speedOfLight = 299_792_458;
const avogadro = 6.022_140_76e23;
const planck = 6.626_070_15e-34;
Long physical constants become readable.
Version numbers (careful):
// ❌ These aren't numbers
const version = 1.2.3; // syntax error
// ✅ These are strings
const version = '1.2.3';
// ✅ But numeric parts can use separators
const buildNumber = 1_234_567;
Version numbers with multiple dots aren’t numbers — they’re strings or objects. Numeric separators don’t apply.
A comparison of before and after:
// Before — hard to read
const population = 8045311447;
const pi = 3.141592653589793;
const bigId = 9007199254740991n;
// After — clear at a glance
const population = 8_045_311_447;
const pi = 3.141_592_653_589_793;
const bigId = 9_007_199_254_740_991n;
Same values. Much easier to verify.
Putting it all together:
const BYTE = 1;
const KB = 1_024;
const MB = 1_048_576;
const GB = 1_073_741_824;
const oneYearInMs = 31_536_000_000;
const maxSafeInt = 9_007_199_254_740_991n;
const hexWhite = 0xFF_FF_FF;
const permissions = 0o7_5_5;
const flags = 0b1111_1111_0000_0000;
Each example uses a grouping that matches its domain. The result is code that’s easier to read and harder to get wrong.
Complete Example Session
// ============================================
// PART 1: BASIC INTEGER
// ============================================
const million = 1_000_000;
console.log(million);
// [ 1000000 ]
console.log(million === 1000000);
// [ true ]
// ============================================
// PART 2: FLOAT
// ============================================
const pi = 3.141_592_653;
console.log(pi);
// [ 3.141592653 ]
// ============================================
// PART 3: HEX
// ============================================
const white = 0xFF_FF_FF;
console.log(white);
// [ 16777215 ]
// ============================================
// PART 4: BINARY
// ============================================
const mask = 0b1111_0000;
console.log(mask);
// [ 240 ]
// ============================================
// PART 5: BIGINT
// ============================================
const big = 9_007_199_254_740_991n;
console.log(big);
// [ 9007199254740991n ]
// ============================================
// PART 6: EXPONENT
// ============================================
const avogadro = 6.022_140_76e23;
console.log(avogadro);
// [ 6.02214076e+23 ]
// ============================================
// PART 7: ILLEGAL — CONSECUTIVE
// ============================================
try {
eval('1__000');
} catch (err) {
console.log(err.message);
}
// [ SyntaxError: Numeric separator can not be used after a numeric literal
// (or similar engine message) ]
// ============================================
// PART 8: ILLEGAL — TRAILING
// ============================================
try {
eval('1_000_');
} catch (err) {
console.log(err.name);
}
// [ 'SyntaxError' ]
// ============================================
// PART 9: NUMBER() DOESN'T PARSE
// ============================================
console.log(Number('1_000'));
// [ NaN ]
console.log(Number('1000'));
// [ 1000 ]
// ============================================
// PART 10: NO EFFECT ON MATH
// ============================================
const a = 1_000_000;
const b = 1_000;
console.log(a / b);
// [ 1000 ]
// ============================================
// PART 11: JSON
// ============================================
console.log(JSON.stringify({ price: 1_000 }));
// [ '{"price":1000}' ]
// ============================================
// PART 12: HEX COLOR
// ============================================
const red = 0xFF_00_00;
const green = 0x00_FF_00;
const blue = 0x00_00_FF;
console.log(red.toString(16));
// [ 'ff0000' ]
// ============================================
// PART 13: FILE SIZE
// ============================================
const KB = 1_024;
const MB = 1_048_576;
const GB = 1_073_741_824;
console.log(GB);
// [ 1073741824 ]
// ============================================
// PART 14: TIMESTAMP
// ============================================
const jan2024 = 1_704_067_200;
console.log(new Date(jan2024 * 1000).toISOString());
// [ '2024-01-01T00:00:00.000Z' ]
// ============================================
// PART 15: BIGINT ID
// ============================================
const id = 1_234_567_890_123_456_789n;
console.log(typeof id);
// [ 'bigint' ]
// ============================================
// PART 16: GROUPING CONVENTION
// ============================================
const grouped = 1_000_000_000;
const grouped2 = 10_00_00_00_00; // legal but unusual
console.log(grouped === grouped2);
// [ true ]
Quick Reference
Where Separators Work
| Literal | Example | Valid? |
|---|---|---|
| Integer | 1_000 | ✅ |
| Float | 1_000.5 | ✅ |
| Hex | 0xFF_FF | ✅ |
| Binary | 0b1010_1010 | ✅ |
| Octal | 0o7_5_5 | ✅ |
| BigInt | 1_000n | ✅ |
| Exponent | 1_000e3 | ✅ |
Where They Don’t Work
| Case | Example | Result |
|---|---|---|
| Consecutive | 1__000 | ❌ SyntaxError |
| Leading | _1000 | ❌ invalid identifier |
| Trailing | 1000_ | ❌ SyntaxError |
| After prefix | 0x_FF | ❌ SyntaxError |
| Before suffix | 1000_n | ❌ SyntaxError |
| Next to dot | 1_.5 | ❌ SyntaxError |
Next to e | 1e_3 | ❌ SyntaxError |
Runtime Behavior
| Operation | Result |
|---|---|
1_000 === 1000 | true |
typeof 1_000 | 'number' |
1_000 + 1 | 1001 |
JSON.stringify(1_000) | '1000' |
Number('1_000') | NaN |
parseInt('1_000') | 1 |
Common Groupings
| Domain | Grouping | Example |
|---|---|---|
| General | 3 | 1_000_000 |
| Hex | 2 or 4 | 0xFF_FF |
| Binary | 4 or 8 | 0b1111_0000 |
| Credit cards | 4 | 4_242_4242_4242_4242 |
| Phone | 3-4 | 1_800_555_0199 |
| Timestamps | 3 | 1_704_067_200 |
| Bytes | 3 | 1_073_741_824 |
Rules
| Rule | Meaning |
|---|---|
| Between digits | Must have digit on both sides |
| No consecutive | Only one at a time |
| Not adjacent to prefix | 0x, 0b, 0o |
| Not adjacent to suffix | n for BigInt |
| Not adjacent to dot | Not before or after . |
Not adjacent to e | Not before or after exponent marker |
Value Impact
| Aspect | Impact |
|---|---|
| Runtime | None |
| Bundle size | None |
| Memory | None |
| Parsing | Ignored |
| JSON | Not allowed |
Best Practices
✅ Do This:
// Use for large numbers
const million = 1_000_000; // ✅
// Use for byte sizes
const GB = 1_073_741_824; // ✅
// Use for BigInt
const id = 9_007_199_254_740_991n; // ✅
// Use for hex masks
const mask = 0xFF_FF_00_00; // ✅
// Use for binary flags
const flags = 0b1111_1111_0000_0000; // ✅
// Use for timestamps
const stamp = 1_704_067_200; // ✅
// Use for milliseconds
const DAY = 86_400_000; // ✅
// Group by domain convention
const cc = 4_242_4242_4242_4242; // ✅
❌ Don’t Do This:
// Don't use separators on small numbers
const n = 1_0; // ⚠️ noise
// Don't break the rules
const bad = 1__000; // ❌ SyntaxError
// Don't put them in strings and expect parsing
Number('1_000'); // ❌ NaN
// Don't emit them in JSON
// {'price': 1_000} // ❌ invalid JSON
// Don't group inconsistently within a file
1_000_000 // ✅
10_00_000 // ⚠️ confusing
// Don't use them on version numbers
1.2.3 // ❌ not a number
// Don't put separators next to prefixes
0x_FF // ❌ SyntaxError
// Don't waste them on tiny numbers
const x = 1_2; // ⚠️ pointless
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Number('1_000') | NaN | Only works in source |
| JSON with separators | Invalid | Serialize without |
| Leading underscore | Syntax error | Start with digit |
| Trailing underscore | Syntax error | End with digit |
| Multiple underscores | Syntax error | One between digits |
| Adjacent to prefix | Syntax error | Start after first digit |
Adjacent to n | Syntax error | Put before suffix |
Adjacent to . | Syntax error | Only between digits |
Real-World Examples
1. Money
const balance = 1_234_567.89;
Group by thousands — easy to read at a glance.
2. Byte sizes
const GB = 1_073_741_824;
const TB = 1_099_511_627_776;
Exact binary multiples — no confusion with 1000-based units.
3. Millisecond constants
const MINUTE = 60_000;
const HOUR = 3_600_000;
const DAY = 86_400_000;
Time math with readable constants.
4. Unix timestamps
const Y2024 = 1_704_067_200;
const Y2025 = 1_735_689_600;
Ten-digit timestamps grouped by thousands.
5. Snowflake IDs
const userId = 1_234_567_890_123_456_789n;
BigInt — separated for readability.
6. Hex colors
const white = 0xFF_FF_FF;
const teal = 0x00_80_80;
Byte-aligned grouping.
7. Bitmask flags
const READ = 0b0001;
const WRITE = 0b0010;
const EXEC = 0b0100;
Binary with one bit per flag.
8. Grouped binary word
const word = 0b1010_1010_1010_1010;
Grouped by nibbles.
9. Credit card test
const testCard = 4_242_4242_4242_4242;
Grouped like the printed card.
10. Scientific constant
const c = 299_792_458;
const planck = 6.626_070_15e-34;
Long physical constants become scannable.
Visual: How Parsing Works
┌──────────────────────────────────────────────┐
│ Source code: 1_000_000 │
│ │
│ Tokenizer: reads digits, ignores _ │
│ │
│ Parser: receives 1000000 │
│ │
│ Runtime: number 1000000 │
│ │
│ The underscore never exists at runtime │
│ │
└──────────────────────────────────────────────┘
Visual: Grouping by Domain
┌──────────────────────────────────────────────┐
│ Money: 1_234_567.89 │
│ Timestamp: 1_704_067_200 │
│ Byte size: 1_073_741_824 │
│ Hex color: 0xFF_00_AA │
│ Binary: 0b1111_0000_1010_0101 │
│ BigInt: 9_007_199_254_740_991n │
│ Credit card: 4_242_4242_4242_4242 │
│ Phone: 1_800_555_0199 │
│ │
│ Grouping matches the domain │
│ │
└──────────────────────────────────────────────┘
Visual: Legal vs Illegal
┌──────────────────────────────────────────────┐
│ ✅ Legal │
│ │
│ 1_000 │
│ 1_000_000 │
│ 1_000.000_1 │
│ 0xFF_FF │
│ 0b1010_1010 │
│ 1_000n │
│ 1_000e3 │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ ❌ Illegal │
│ │
│ 1__000 consecutive │
│ _1_000 leading │
│ 1_000_ trailing │
│ 0x_FF after prefix │
│ 0b_1010 after prefix │
│ 1_000_n before suffix │
│ 1_.000 before dot │
│ 1._000 after dot │
│ 1_e3 before e │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Syntax | Example |
|---|---|---|
| Integer | 1_000 | 1_000_000 |
| Float | 1_000.5 | 3.141_592 |
| Hex | 0xFF_FF | 0x00_80_80 |
| Binary | 0b1010_1010 | 0b1111_0000 |
| Octal | 0o7_5_5 | 0o755_644 |
| BigInt | 1_000n | 9_007_199_254_740_991n |
| Exponent | 1_000e3 | 6.022_140_76e23 |
| Rule | Between digits | Both sides digit |
| Runtime | Ignored | 1_000 === 1000 |
| JSON | Not allowed | Use 1000 |
Key takeaways:
- Numeric separators are underscores in numeric literals — ignored completely at runtime
- They work on integers, floats, hex, binary, octal, BigInt, and exponents
- A separator must sit between two digits — no leading, trailing, consecutive, or adjacent to prefixes, suffixes, dots, or
e 1_000 === 1000is true — no runtime effect, no bundle size changeNumber('1_000')returnsNaN— separators are a syntax feature, not a parsing one- JSON doesn’t allow separators — serialize without them
- Grouping is your choice — match the domain: thousands for money, nibbles for binary, chunks of four for credit cards
- Use for large numbers, hex colors, bitmasks, byte sizes, timestamps, BigInt IDs
- Skip for small numbers where the underscore is noise
- Adopt a style rule — 5+ digits, or always for hex/binary — and stay consistent
- The feature is purely for readability — but readability catches bugs before they ship
Remember: Numeric separators don’t change behavior — they change how your code reads. 1_000_000 and 1000000 are the same number, but only one of them tells you at a glance that it’s a million. Use them for money, byte sizes, timestamps, IDs, hex colors, and binary masks. Match the grouping to the domain. Adopt a consistent style. And remember they exist only in source code — never in strings, JSON, or runtime input. Small feature, real payoff.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!