jsmanifest logojsmanifest

JavaScript Numeric Separators and Other Readability Features

JavaScript Numeric Separators and Other Readability Features

Learn how JavaScript's numeric separators and ES2021+ readability features transform the way we write maintainable code—with practical examples from financial apps and scientific computing.

While I was looking over some legacy code in a financial application the other day, I came across something that made me pause: const maxBudget = 10000000000;. I stared at it for a good ten seconds, counting zeros like a confused mathematician. Is that ten billion? One billion? My eyes glazed over, and I realized this was a problem I'd been dealing with for years without acknowledging it.

Little did I know that JavaScript had already solved this exact problem with numeric separators—a feature I'd somehow missed despite it landing in ES2021. I was once guilty of writing numbers that looked like eye tests, and I cannot stress this enough: readable code isn't just about variable names and comments. Sometimes it's about the numbers themselves.

Why Numeric Readability Matters in Modern JavaScript

Let me paint you a picture. You're maintaining a payment processing system, and you need to verify that transaction limits are correctly set. You see this:

const dailyLimit = 25000;
const monthlyLimit = 750000;
const annualLimit = 9000000;

Quick—which of these is wrong? The daily limit should be $250.00, monthly should be $7,500.00, and annual should be $90,000.00. But wait, did I get that right? Are we talking about cents or dollars?

This is where the real cost of poor readability hits. It's not just about aesthetics—it's about preventing bugs that cost real money. When I finally decided to audit some of our critical business logic, I found three separate instances where developers had added or removed zeros incorrectly. One of them had been sitting there for eight months.

The cognitive load of parsing large numbers without visual grouping is higher than we think. Our brains naturally group digits by threes (in Western contexts), yet we force developers to count individual characters. That's backwards.

JavaScript numeric separators improving code readability

Understanding Numeric Separators: The Underscore Revolution

Here's where things get wonderful. JavaScript's numeric separators let you use underscores (_) to visually group digits in numeric literals. The interpreter completely ignores these underscores—they're purely for us humans.

When I realized I could write 1_000_000 instead of 1000000, something clicked. This wasn't just syntactic sugar; this was JavaScript finally acknowledging that developers are human beings who need to read their own code.

The underscore acts as a visual separator without affecting the actual value:

// All of these are exactly the same value
const billion = 1000000000;
const readableBillion = 1_000_000_000;
 
console.log(billion === readableBillion); // true
 
// You can group however makes sense for your domain
const creditCardNumber = 1234_5678_9012_3456;
const ssn = 123_45_6789;
const binary = 0b1010_0001_1000_0101;

Luckily we can use numeric separators with any numeric literal type in {/* REMOVED: JavaScript: */} decimal, binary, octal, hexadecimal, and even BigInt. The flexibility here is fascinating because different domains have different grouping conventions.

Practical Examples: Decimal, Binary, Hex, and BigInt

Let me show you how I refactored that financial code I mentioned earlier. The transformation was remarkable:

// Before: Please don't make me count these zeros
const API_RATE_LIMIT_PER_HOUR = 100000;
const DATABASE_MAX_CONNECTIONS = 10000;
const CACHE_SIZE_BYTES = 536870912;
const COMPANY_VALUATION = 15000000000;
 
// After: Instant clarity
const API_RATE_LIMIT_PER_HOUR = 100_000;
const DATABASE_MAX_CONNECTIONS = 10_000;
const CACHE_SIZE_BYTES = 536_870_912; // 512 MB
const COMPANY_VALUATION = 15_000_000_000; // 15 billion
 
// Scientific constants become readable
const SPEED_OF_LIGHT = 299_792_458; // meters per second
const AVOGADRO = 6.022_140_76e23;
 
// Binary flags for bit masking
const READ_PERMISSION = 0b0000_0100;
const WRITE_PERMISSION = 0b0000_0010;
const EXECUTE_PERMISSION = 0b0000_0001;
 
// Hexadecimal color codes
const BRAND_PRIMARY = 0xFF_6B_35;
const BRAND_SECONDARY = 0x00_4E_89;
 
// BigInt for truly massive numbers
const NATIONAL_DEBT = 31_400_000_000_000n;
const CRYPTOCURRENCY_SUPPLY = 21_000_000_000_000_000_000n;

In other words, numeric separators adapt to your domain's conventions. Financial code groups by thousands. Binary code groups by nibbles (4 bits) or bytes (8 bits). Color codes group by RGB components. This flexibility is what makes the feature so valuable.

Practical examples of numeric separators in different contexts

Beyond Numeric Separators: Other ES2021+ Readability Features

While numeric separators solved my immediate problem, I discovered they were part of a broader trend in modern JavaScript toward prioritizing developer experience and code clarity.

The replaceAll method arrived in ES2021 alongside numeric separators. Before this, I was doing this dance with regex or split-join combinations:

// The old way (still works but verbose)
const oldApproach = 'foo-bar-baz'.split('-').join('_');
 
// The new, clearer way
const newApproach = 'foo-bar-baz'.replaceAll('-', '_');

Logical assignment operators (||=, &&=, ??=) also landed in ES2021, making default value assignments more expressive. I came across code like this constantly:

// Before: Works but requires mental parsing
options.timeout = options.timeout || 5000;
 
// After: Intent is crystal clear
options.timeout ||= 5000;

The ??= operator is particularly wonderful for distinguishing between "falsy" and "nullish" values—something that tripped me up when dealing with configuration objects where 0 or false were valid values.

Real-World Use Cases: Financial Apps, Scientific Computing, and More

Let me share where numeric separators made the biggest impact in my work. I was building a cryptocurrency trading dashboard, and the numbers were all over the place—some in satoshis (100 millionth of a Bitcoin), some in wei (smallest Ethereum unit), and some in dollars.

// Cryptocurrency units are notoriously hard to read
const ONE_BITCOIN_IN_SATOSHIS = 100_000_000;
const ONE_ETHER_IN_WEI = 1_000_000_000_000_000_000n;
 
// Trading limits become instantly parseable
const MIN_TRADE_AMOUNT_USD = 10_00; // $10.00 in cents
const MAX_TRADE_AMOUNT_USD = 100_000_00; // $100,000.00 in cents
const WHALE_THRESHOLD = 1_000_000_00; // $1M in cents
 
// Gas limits for blockchain transactions
const SIMPLE_TRANSFER_GAS = 21_000;
const COMPLEX_CONTRACT_GAS = 500_000;
 
function calculateTradeValue(amount: number, priceInCents: number): number {
  const maxSafeAmount = 9_007_199_254_740_991; // Number.MAX_SAFE_INTEGER
  
  if (amount > maxSafeAmount) {
    throw new Error('Amount exceeds safe integer range');
  }
  
  return (amount * priceInCents) / 100;
}

In scientific computing, the readability gains are even more dramatic. Physical constants and measurements become self-documenting:

const PLANCK_CONSTANT = 6.626_070_15e-34; // Joule-seconds
const EARTH_MASS_KG = 5.972_2e24;
const LIGHT_YEAR_METERS = 9_460_730_472_580_800;

I cannot stress this enough: when you're dealing with numbers that have real-world consequences—whether that's money, scientific calculations, or system limits—every bit of clarity helps prevent catastrophic errors.

Comparing Readability Approaches: Before and After ES2021

The transformation in my codebase wasn't just about individual numbers—it changed how I approached numeric constants entirely. Let me show you a before-and-after from a configuration file:

// Before ES2021: Comments were mandatory for clarity
const config = {
  maxFileSize: 10485760, // 10 MB in bytes
  timeout: 30000, // 30 seconds in milliseconds
  rateLimitPerMinute: 1000, // requests
  cacheSize: 134217728, // 128 MB
  retryDelay: 5000 // 5 seconds
};
 
// After ES2021: Numbers speak for themselves
const config = {
  maxFileSize: 10_485_760, // Still nice to mention "10 MB" but optional
  timeout: 30_000,
  rateLimitPerMinute: 1_000,
  cacheSize: 134_217_728,
  retryDelay: 5_000
};

The difference is subtle but profound. Comments that existed purely to help humans parse numbers could now be reserved for explaining why these particular values were chosen, not what they represent.

Best Practices and Common Pitfalls to Avoid

As I adopted numeric separators across my projects, I learned some valuable lessons the hard way. Here are the gotchas that caught me:

You cannot place separators at the start or end of a number, or have consecutive separators:

// These will cause syntax errors
const invalid1 = _100_000; // No leading underscore
const invalid2 = 100_000_; // No trailing underscore
const invalid3 = 100__000; // No consecutive underscores
 
// But these are fine
const valid1 = 100_000;
const valid2 = 0.123_456;
const valid3 = 1_2_3_4_5_6; // Technically valid but weird

I was once guilty of inconsistent grouping within the same module. Pick a convention and stick with it:

// Inconsistent and confusing
const price1 = 1_23_456; // Grouping by twos and threes?
const price2 = 123_456; // No grouping on the left
const price3 = 1_234_56; // Different pattern entirely
 
// Consistent and clear
const price1 = 123_456;
const price2 = 123_456;
const price3 = 1_234_56; // Cents separated

When working with APIs or parsing user input, remember that parseInt and parseFloat don't support numeric separators in strings:

// This works (numeric literal)
const literal = 1_000_000;
 
// This fails (string parsing)
const parsed = parseInt('1_000_000'); // Returns 1, not 1000000
const corrected = parseInt('1_000_000'.replace(/_/g, '')); // Returns 1000000

Embracing Modern JavaScript for Maintainable Codebases

The journey from discovering numeric separators to making them a core part of my coding style taught me something important: readability features aren't luxuries—they're investments in future maintainability. Every time I write 1_000_000 instead of 1000000, I'm saving the next developer (often future me) from counting zeros.

Little did I know that such a simple feature would change how I think about numeric literals entirely. When I finally decided to refactor our legacy financial code with numeric separators, the code review revealed three bugs we hadn't caught in months—all related to miscounted digits.

The broader lesson here is wonderful: modern JavaScript isn't just about new capabilities; it's about acknowledging that code is read far more often than it's written. Features like numeric separators, logical assignment operators, and replaceAll might seem small, but they compound into significantly more maintainable codebases.

I encourage you to audit your codebase for opportunities to improve numeric readability. Look for large constants, configuration objects, and anywhere you've added comments just to explain what a number means. These are prime candidates for numeric separators.

And that concludes the end of this post! I hope you found this valuable and look out for more in the future!