Nullish Coalescing, Optional Chaining, and BigInts

Mikey NicholsMikey Nichols
6 min read

JavaScript's evolution continues to bring us powerful features that make our code more robust and readable. Let's dive into some of the most impactful additions from ES11 (ECMAScript 2020) and ES12 (ECMAScript 2021) that are transforming how we write JavaScript.

JavaScript becomes more robust, error-resistant, and expressive

JavaScript has come a long way since ES6 laid the groundwork for modern development patterns. While ES6 brought revolutionary changes with arrow functions, classes, promises, and modules, recent versions have focused on solving common pain points and improving developer experience. ES11 and ES12 continue this trend with features that make JavaScript more robust, error-resistant, and expressive.

ES11/ECMAScript 2020: Quality-of-Life Improvements

Optional Chaining (?.)

Remember the days of writing defensive code to check nested object properties? Optional chaining has revolutionized how we handle these scenarios.

// Old way
const city = user && user.address && user.address.city;

// Modern way with optional chaining
const city = user?.address?.city;

// Works with function calls too
const result = someObject?.someMethod?.();

// Array access
const firstElement = array?.[0];

This elegant syntax not only makes our code more readable but also helps prevent the dreaded "Cannot read property 'x' of undefined" errors that have plagued JavaScript developers for years.

Nullish Coalescing: Smarter Default Values

The nullish coalescing operator (??) provides a more precise way to handle default values compared to the logical OR operator (||).

// Consider these values
const count = 0;
const text = "";
const nullValue = null;
const undefinedValue = undefined;

// Logical OR vs Nullish Coalescing
console.log(count || 10);         // 10 (undesired!)
console.log(count ?? 10);         // 0 (correct!)

console.log(text || "default");   // "default" (undesired!)
console.log(text ?? "default");   // "" (correct!)

console.log(nullValue ?? "default");       // "default"
console.log(undefinedValue ?? "default");  // "default"

The key difference is that nullish coalescing only falls back to the default value when the left-hand side is null or undefined, making it perfect for cases where 0 or empty strings are valid values.

Promise.allSettled(): Better Promise Handling

When dealing with multiple promises, Promise.allSettled() provides a more complete solution than Promise.all():

const promises = [
  fetch('/api/users'),
  fetch('/api/products'),
  fetch('/api/orders')
];

const results = await Promise.allSettled(promises);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`Promise ${index} succeeded:`, result.value);
  } else {
    console.log(`Promise ${index} failed:`, result.reason);
  }
});

Unlike Promise.all() which fails fast if any promise rejects, Promise.allSettled() always waits for all promises to complete, giving you full control over how to handle both successes and failures.

BigInt: Handling Large Numbers

For applications dealing with large numbers (like financial calculations or scientific computing), BigInt provides a way to work with numbers beyond Number.MAX_SAFE_INTEGER:

// Creating BigInts
const bigNumber = 9007199254740991n;  // Using 'n' suffix
const alsoBig = BigInt("9007199254740991");

// Operations
const result = bigNumber + 1n;
console.log(result);  // 9007199254740992n

// Mixing with regular numbers requires explicit conversion
const mixed = BigInt(Number.MAX_SAFE_INTEGER) + 1n;

// Common use case: Working with large IDs
const userId = 9007199254740995n;
const nextId = userId + 1n;

Dynamic Imports: Loading Code On Demand

Dynamic imports allow you to load modules conditionally, improving initial load times:

async function loadModule() {
  try {
    if (user.preferences.theme === 'dark') {
      const { darkTheme } = await import('./themes/dark.js');
      darkTheme.apply();
    }
  } catch (error) {
    console.error('Error loading theme:', error);
  }
}

This feature is perfect for code splitting, lazy loading, and conditionally loading features based on user preferences or device capabilities.

globalThis: Consistent Global Reference

The globalThis object provides a standard way to access the global object across different JavaScript environments:

// Before globalThis
const getGlobalObject = () => {
  if (typeof window !== 'undefined') return window;
  if (typeof global !== 'undefined') return global;
  if (typeof self !== 'undefined') return self;
  throw new Error('Unable to locate global object');
};

// With globalThis
console.log(globalThis); // Points to window, global, or self depending on environment

This eliminates the need for environment detection code when accessing global variables or features.

ES12/ECMAScript 2021: Polishing the Language

String.prototype.replaceAll(): Better String Manipulation

Before ES12, replacing all occurrences of a string required using a regular expression with the global flag:

javascript// Before replaceAll
const string = "Hello World! Hello JavaScript!";
const replaced = string.replace(/Hello/g, "Hi");

// With replaceAll
const cleanReplaced = string.replaceAll("Hello", "Hi");

This method provides a cleaner, more intuitive way to perform global string replacements without needing to understand regular expressions.

Numeric Separators: Readable Numbers

Numeric separators make large numbers more readable by allowing underscores as separators:

// Hard to read
const billion = 1000000000;

// Easy to read
const readableBillion = 1_000_000_000;

// Works with decimals
const pi = 3.141_592_653_589;

// And hex, binary, octal
const hex = 0xFF_FF_FF; // 16777215
const binary = 0b0101_0101; // 85

This feature has no runtime impact—it's purely for improving code readability.

Logical Assignment Operators: Combining Operations

Logical assignment operators combine logical operations with assignment, reducing boilerplate code:

// Before
if (!obj.prop) obj.prop = "default";  // ||=
if (obj.prop) obj.prop = newValue;    // &&=
if (obj.prop === null || obj.prop === undefined) obj.prop = "default"; // ??=

// After
obj.prop ||= "default";
obj.prop &&= newValue;
obj.prop ??= "default";

These operators are particularly useful for conditionally updating values in a concise way.

WeakRefs and FinalizationRegistry: Advanced Memory Management

ES12 introduced tools for more granular control over memory management:

// Creating a weak reference
const weakRef = new WeakRef(someObject);

// Accessing the target if it still exists
const obj = weakRef.deref();
if (obj) {
  console.log("Object still in memory");
}

// Setting up cleanup when an object is garbage collected
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Object with value ${heldValue} has been garbage collected`);
});

// Register an object to be tracked
registry.register(someObject, "metadata about the object");

While these are advanced features not needed in everyday code, they enable better memory management patterns for complex applications.

Best Practices for Modern JavaScript

Combining Features for Robust Code

The real power of these new features comes when combining them:

// Combining optional chaining, nullish coalescing, and logical assignment
// Before
let value;
if (data && data.user && data.user.preferences) {
  value = data.user.preferences.theme || 'default';
}

// After
const value = data?.user?.preferences?.theme ?? 'default';

// With assignment
config.theme ??= user?.preferences?.theme ?? 'default';

Progressive Enhancement Strategies

When using these modern features, consider browser compatibility:

// Feature detection
const hasOptionalChaining = () => {
  try {
    eval('({}).?prop');
    return true;
  } catch (e) {
    return false;
  }
};

// Conditional code
if (hasOptionalChaining()) {
  // Use optional chaining
} else {
  // Use fallback approach
}

Looking Forward: What's Next for JavaScript?

JavaScript continues to evolve with exciting proposals in the pipeline:

  • Record and Tuple types for immutable data structures

  • Pattern matching for more expressive conditionals

  • Decorators for metaprogramming capabilities

  • Pipeline operator for more functional programming patterns

The influence of TypeScript is also increasingly apparent in JavaScript's evolution, with more attention to type safety and developer tooling.

Conclusion: The Maturing JavaScript Ecosystem

ES11 and ES12 have significantly improved JavaScript's robustness and expressiveness. These quality-of-life features address long-standing pain points and make our code more readable, maintainable, and error-resistant.

The cumulative effect of these incremental improvements is a more mature language that's better equipped for building complex applications. We're seeing reduced boilerplate, more intuitive APIs, and improved developer experience across the board.

As JavaScript continues to evolve, we can expect further refinements that balance the language's flexibility with the need for reliability and maintainability in modern web development.

What modern JavaScript features do you find most useful in your projects? Let us know in the comments below!

0
Subscribe to my newsletter

Read articles from Mikey Nichols directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mikey Nichols
Mikey Nichols

I am an aspiring web developer on a mission to kick down the door into tech. Join me as I take the essential steps toward this goal and hopefully inspire others to do the same!