Nullish Coalescing, Optional Chaining, and BigInts


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!
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!