| |

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:

LiteralWith separator
Integer1_000
Float1_000.5
Hex0xFF_FF
Binary0b1010_1010
Octal0o7_5_5
BigInt1_000n
Exponent1_000e3

A note on grouping:

There’s no rule about where to put separators — you choose. Common conventions:

DomainGroupingExample
General3 (thousands)1_000_000
Hex4 or 80xFF_FF_FF_FF
Binary4 or 80b1111_0000
Credit cards44_242_4242_4242_4242
Phone numbers3-4555_123_4567
Timestamps31_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_824 for 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:

DomainConvention
MoneyGroup by 3 (thousands)
Hex colorsGroup by 2 (0xFF_00_AA)
Binary flagsGroup by 4 or 8
IDsGroup by 3
Byte sizesGroup by 3 (1_073_741_824)
Credit cardsGroup by 4
Phone numbersGroup by 3-4
TimestampsGroup 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 2024 for 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

LiteralExampleValid?
Integer1_000
Float1_000.5
Hex0xFF_FF
Binary0b1010_1010
Octal0o7_5_5
BigInt1_000n
Exponent1_000e3

Where They Don’t Work

CaseExampleResult
Consecutive1__000❌ SyntaxError
Leading_1000❌ invalid identifier
Trailing1000_❌ SyntaxError
After prefix0x_FF❌ SyntaxError
Before suffix1000_n❌ SyntaxError
Next to dot1_.5❌ SyntaxError
Next to e1e_3❌ SyntaxError

Runtime Behavior

OperationResult
1_000 === 1000true
typeof 1_000'number'
1_000 + 11001
JSON.stringify(1_000)'1000'
Number('1_000')NaN
parseInt('1_000')1

Common Groupings

DomainGroupingExample
General31_000_000
Hex2 or 40xFF_FF
Binary4 or 80b1111_0000
Credit cards44_242_4242_4242_4242
Phone3-41_800_555_0199
Timestamps31_704_067_200
Bytes31_073_741_824

Rules

RuleMeaning
Between digitsMust have digit on both sides
No consecutiveOnly one at a time
Not adjacent to prefix0x, 0b, 0o
Not adjacent to suffixn for BigInt
Not adjacent to dotNot before or after .
Not adjacent to eNot before or after exponent marker

Value Impact

AspectImpact
RuntimeNone
Bundle sizeNone
MemoryNone
ParsingIgnored
JSONNot 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

PitfallProblemSolution
Number('1_000')NaNOnly works in source
JSON with separatorsInvalidSerialize without
Leading underscoreSyntax errorStart with digit
Trailing underscoreSyntax errorEnd with digit
Multiple underscoresSyntax errorOne between digits
Adjacent to prefixSyntax errorStart after first digit
Adjacent to nSyntax errorPut before suffix
Adjacent to .Syntax errorOnly 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

ConceptSyntaxExample
Integer1_0001_000_000
Float1_000.53.141_592
Hex0xFF_FF0x00_80_80
Binary0b1010_10100b1111_0000
Octal0o7_5_50o755_644
BigInt1_000n9_007_199_254_740_991n
Exponent1_000e36.022_140_76e23
RuleBetween digitsBoth sides digit
RuntimeIgnored1_000 === 1000
JSONNot allowedUse 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 === 1000 is true — no runtime effect, no bundle size change
  • Number('1_000') returns NaN — 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!