JavaScript ES2021 Features: What's New and How to Use Them
JavaScript ES2021 Features: What's New and How to Use Them
ES2021 (ECMAScript 2021) brings several exciting new features to JavaScript. In this article, we'll explore each new feature with practical examples and learn how they can improve your code.
Overview of ES2021 Features
ES2021 introduces five new features:
- Logical Assignment Operators (
??=,||=,&&=) Promise.any()andAggregateErrorString.prototype.replaceAll()- Numeric Separators
WeakRefandFinalizationRegistry
Let's dive into each one.
1. Logical Assignment Operators
ES2021 introduces three new logical assignment operators that combine logical operations with assignment.
Nullish Coalescing Assignment (??=)
Assigns only if the current value is null or undefined:
let user = null;
user ??= 'Anonymous';
console.log(user); // 'Anonymous'
let count = 0;
count ??= 10;
console.log(count); // 0 (not assigned because 0 is not null/undefined)
// Equivalent to:
if (user === null || user === undefined) {
user = 'Anonymous';
}
Logical OR Assignment (||=)
Assigns only if the current value is falsy:
let title = '';
title ||= 'Untitled';
console.log(title); // 'Untitled'
let count = 0;
count ||= 1;
console.log(count); // 1 (0 is falsy, so assignment happens)
let options = { debug: false };
options.debug ||= true;
console.log(options.debug); // true
// Equivalent to:
if (!title) {
title = 'Untitled';
}
Logical AND Assignment (&&=)
Assigns only if the current value is truthy:
let value = 'hello';
value &&= value.toUpperCase();
console.log(value); // 'HELLO'
let empty = null;
empty &&= 'assigned';
console.log(empty); // null (null is falsy, no assignment)
// Useful for conditional updates
let config = { timeout: 3000 };
config.timeout &&= Math.min(config.timeout, 5000);
console.log(config.timeout); // 3000
// Equivalent to:
if (value) {
value = value.toUpperCase();
}
Practical Example: Configuration Defaults
function setupConnection(options = {}) {
// Set defaults for missing values
options.host ??= 'localhost';
options.port ??= 3000;
options.timeout ??= 5000;
// Enable debug only if not explicitly set
options.debug ||= false;
// Apply validation only if retryCount exists
options.retryCount &&= Math.min(options.retryCount, 10);
return options;
}
setupConnection({ host: 'api.example.com' });
// { host: 'api.example.com', port: 3000, timeout: 5000, debug: false }
2. Promise.any() and AggregateError
Promise.any() accepts an array of promises and resolves as soon as any of the promises fulfills.
Basic Usage
const promises = [
fetch('/api/endpoint1'),
fetch('/api/endpoint2'),
fetch('/api/endpoint3')
];
Promise.any(promises)
.then(firstResponse => {
console.log('First successful response:', firstResponse);
})
.catch(error => {
console.log('All promises rejected:', error);
});
Comparison with Other Promise Methods
// Promise.all() - Waits for ALL to fulfill, rejects if ANY rejects
Promise.all([p1, p2, p3]);
// Promise.race() - Resolves/rejects with the FIRST settled promise
Promise.race([p1, p2, p3]);
// Promise.allSettled() - Waits for ALL to settle, never rejects
Promise.allSettled([p1, p2, p3]);
// Promise.any() - Resolves with the FIRST fulfillment, rejects if ALL reject
Promise.any([p1, p2, p3]);
AggregateError
When all promises reject, Promise.any() throws an AggregateError:
const failingPromises = [
Promise.reject(new Error('Server 1 down')),
Promise.reject(new Error('Server 2 down')),
Promise.reject(new Error('Server 3 down'))
];
try {
const result = await Promise.any(failingPromises);
} catch (error) {
console.log(error instanceof AggregateError); // true
console.log(error.errors); // Array of all rejection reasons
// [Error: Server 1 down, Error: Server 2 down, Error: Server 3 down]
}
Practical Example: Multiple API Endpoints
async function fetchWithFallback(urls) {
try {
const response = await Promise.any(
urls.map(url => fetch(url).then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res;
}))
);
return await response.json();
} catch (aggregateError) {
console.error('All endpoints failed:', aggregateError.errors);
throw new Error('No available server');
}
}
// Try multiple CDN endpoints
const data = await fetchWithFallback([
'https://cdn1.example.com/data.json',
'https://cdn2.example.com/data.json',
'https://cdn3.example.com/data.json'
]);
3. String.prototype.replaceAll()
Before ES2021, replacing all occurrences of a substring required using regular expressions with the global flag.
The Problem with replace()
const text = 'The quick brown fox jumps over the lazy fox.';
// replace() only replaces the first occurrence
text.replace('fox', 'cat');
// 'The quick brown cat jumps over the lazy fox.'
// Had to use regex for all occurrences
text.replace(/fox/g, 'cat');
// 'The quick brown cat jumps over the lazy cat.'
Using replaceAll()
const text = 'The quick brown fox jumps over the lazy fox.';
// Now simple and intuitive
text.replaceAll('fox', 'cat');
// 'The quick brown cat jumps over the lazy cat.'
// No more regex escaping issues
const code = 'function() { return a + b; }';
code.replaceAll('()', '[]');
// 'function[] { return a + b; }'
// With regex, you'd need to escape special characters
code.replace(/\(\)/g, '[]');
Practical Examples
// Sanitizing user input
const userInput = '<script>alert("XSS")</script>';
const sanitized = userInput
.replaceAll('<', '<')
.replaceAll('>', '>');
// '<script>alert("XSS")</script>'
// Template processing
const template = 'Hello {name}, welcome to {place}!';
const result = template
.replaceAll('{name}', 'John')
.replaceAll('{place}', 'our platform');
// 'Hello John, welcome to our platform!'
// Formatting paths
const path = 'folder\\subfolder\\file.txt';
const normalized = path.replaceAll('\\', '/');
// 'folder/subfolder/file.txt'
4. Numeric Separators
Numeric separators make large numbers more readable by allowing underscores between digits.
Basic Usage
// Without separators - hard to read
const billion = 1000000000;
const bytes = 1073741824;
// With separators - much clearer
const billion = 1_000_000_000;
const bytes = 1_073_741_824;
// Works with different number formats
const decimal = 1_000.5;
const hex = 0xFF_FF_FF_FF;
const binary = 0b1010_0001_1000_0101;
const octal = 0o1234_5670;
const bigint = 1_000_000_000n;
Use Cases
// Financial values
const price = 9_999.99;
const budget = 1_500_000.00;
// File sizes
const kilobyte = 1_024;
const megabyte = 1_048_576;
const gigabyte = 1_073_741_824;
// Time constants
const millisecondsPerSecond = 1_000;
const secondsPerMinute = 60;
const secondsPerHour = 3_600;
const secondsPerDay = 86_400;
// Bit masks
const READ_PERMISSION = 0b0000_0001;
const WRITE_PERMISSION = 0b0000_0010;
const EXECUTE_PERMISSION = 0b0000_0100;
// Credit card or ID numbers (for display)
const displayNumber = '1234_5678_9012_3456';
Important Notes
// Separators are removed at runtime
console.log(1_000); // 1000
console.log(1_000 === 1000); // true
// Restrictions
// const invalid1 = _100; // SyntaxError - can't start with _
// const invalid2 = 100_; // SyntaxError - can't end with _
// const invalid3 = 1__000; // SyntaxError - no consecutive underscores
// const invalid4 = 1_.0; // SyntaxError - not next to decimal point
5. WeakRef and FinalizationRegistry
These features provide low-level capabilities for managing memory and cleanup.
WeakRef
A WeakRef holds a weak reference to an object, allowing it to be garbage collected:
let obj = { data: 'important' };
const weakRef = new WeakRef(obj);
// Access the object
console.log(weakRef.deref()); // { data: 'important' }
// After obj is no longer referenced elsewhere
obj = null;
// The object might be garbage collected
console.log(weakRef.deref()); // Could be undefined
FinalizationRegistry
Register cleanup callbacks when objects are garbage collected:
const registry = new FinalizationRegistry((id) => {
console.log(`Object with ID ${id} was garbage collected`);
});
let obj = { data: 'some data' };
registry.register(obj, 'my-object-id');
// When obj is garbage collected, the callback runs
obj = null;
// Eventually: "Object with ID my-object-id was garbage collected"
Practical Use Case: Caching
class WeakCache {
constructor() {
this.cache = new Map();
this.registry = new FinalizationRegistry(key => {
this.cache.delete(key);
});
}
get(key) {
const weakRef = this.cache.get(key);
return weakRef?.deref();
}
set(key, value) {
this.cache.set(key, new WeakRef(value));
this.registry.register(value, key);
}
}
// Usage
const cache = new WeakCache();
cache.set('user-1', { name: 'John', data: new Array(1000000) });
// The cached data can be garbage collected under memory pressure
console.log(cache.get('user-1')); // { name: 'John', data: [...] }
Warning About WeakRef
The documentation explicitly warns that WeakRef should be avoided in most cases:
// AVOID - Checking if an object was collected
function checkCollected(obj) {
const ref = new WeakRef(obj);
return () => ref.deref() === undefined;
}
// This pattern is unreliable and discouraged
WeakRef is primarily useful for:
- Caching large objects
- Holding references to DOM elements
- Implementing weak maps with custom behavior
Browser and Node.js Support
ES2021 features are widely supported:
| Feature | Chrome | Firefox | Safari | Node.js |
|---|---|---|---|---|
| Logical Assignment | 85+ | 79+ | 14+ | 15+ |
| Promise.any() | 85+ | 79+ | 14+ | 15+ |
| replaceAll() | 85+ | 77+ | 13.1+ | 15+ |
| Numeric Separators | 75+ | 70+ | 13+ | 12.5+ |
| WeakRef | 84+ | 79+ | 14+ | 14+ |
Conclusion
ES2021 brings practical features that make JavaScript more expressive and convenient:
- Logical Assignment Operators simplify common assignment patterns
- Promise.any() provides a useful alternative to Promise.race()
- replaceAll() makes string replacement more intuitive
- Numeric Separators improve code readability
- WeakRef enables advanced memory management patterns
Start using these features today to write cleaner, more maintainable JavaScript code!