Difficulty: Intermediate
JavaScript continues to evolve with annual releases that add practical features addressing real-world pain points. ES2020 and subsequent versions introduced several operators and methods that eliminate common patterns of boilerplate code and make the language more expressive. These features are now widely supported in all modern browsers and Node.js versions.
Optional chaining (?.) is perhaps the most impactful addition since async/await. It allows you to safely access deeply nested properties without checking each level for null or undefined. Before optional chaining, accessing user.address.city required checking if user exists, then if user.address exists. With ?., the expression user?.address?.city short-circuits and returns undefined if any part of the chain is nullish. Optional chaining works with property access (obj?.prop), bracket notation (obj?.[expr]), and method calls (obj?.method()).
Nullish coalescing (??) provides a default value specifically when the left operand is null or undefined. Unlike the logical OR (||) operator, which treats any falsy value (0, '', false, NaN) as triggering the fallback, ?? only triggers on null or undefined. This distinction is critical when 0, empty strings, or false are valid values in your application. For example, config.timeout ?? 3000 correctly preserves a timeout of 0, while config.timeout || 3000 would incorrectly override it.
BigInt is a numeric type that can represent integers of arbitrary size, solving the limitation of the Number type which can only safely represent integers up to 2^53 - 1. BigInt literals end with 'n' (like 42n), and you can perform arithmetic on them. However, BigInt and Number cannot be mixed in operations - you must explicitly convert between them. BigInt is essential for cryptography, financial calculations with large numbers, and working with database IDs that exceed Number.MAX_SAFE_INTEGER.
ES2020 also introduced Promise.allSettled(), which waits for all promises to complete regardless of whether they resolve or reject. Unlike Promise.all() which short-circuits on the first rejection, allSettled() gives you the outcome of every promise. Each result object has a status of 'fulfilled' or 'rejected', with either a value or reason property. globalThis provides a universal way to access the global object across environments (window in browsers, global in Node.js, self in workers).
ES2021 brought logical assignment operators (??=, ||=, &&=) that combine logical operations with assignment. x ??= y assigns y to x only if x is null or undefined. x ||= y assigns if x is falsy. x &&= y assigns if x is truthy. These are shorthand for common patterns and make initialization and conditional updates more concise.
ES2022 introduced several practical additions. Array.at() accepts negative indices, making it easy to access elements from the end of an array (arr.at(-1) for the last element). Object.hasOwn() is a static method that replaces the awkward Object.prototype.hasOwnProperty.call() pattern. Top-level await allows using await outside of async functions in ES modules, simplifying module initialization that depends on asynchronous operations.
// Optional chaining - safe property access
const user = {
name: 'Alice',
address: {
city: 'Portland'
}
};
console.log(user?.address?.city);
console.log(user?.phone?.number);
console.log(user?.address?.zipCode?.toString());
// Optional method call
const obj = {
greet: () => 'Hello!'
};
console.log(obj.greet?.());
console.log(obj.farewell?.());
// Optional bracket notation
const data = { users: ['Alice', 'Bob'] };
console.log(data?.users?.[0]);
console.log(data?.admins?.[0]);
// Nullish coalescing vs logical OR
const config = { timeout: 0, title: '', debug: false };
console.log('\n--- ?? vs || ---');
console.log('timeout ??:', config.timeout ?? 3000); // 0 (preserved)
console.log('timeout ||:', config.timeout || 3000); // 3000 (wrong!)
console.log('title ??:', config.title ?? 'Default'); // '' (preserved)
console.log('title ||:', config.title || 'Default'); // Default (wrong!)
console.log('debug ??:', config.debug ?? true); // false (preserved)
console.log('missing ??:', config.missing ?? 'fallback');
Optional chaining returns undefined if any link in the chain is null/undefined, preventing 'Cannot read property of undefined' errors. Nullish coalescing (??) only falls back on null/undefined, correctly preserving falsy values like 0, '', and false.
// BigInt for large integers
console.log('Max safe integer:', Number.MAX_SAFE_INTEGER);
console.log('Overflow:', Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2);
const big1 = 9007199254740993n; // literal
const big2 = BigInt('999999999999999999');
console.log('BigInt addition:', big1 + big2);
console.log('BigInt multiply:', 100n * 200n);
console.log('BigInt compare:', 42n === 42, 42n == 42);
console.log('Type:', typeof 42n);
// Promise.allSettled - get all results
async function fetchAll() {
const results = await Promise.allSettled([
Promise.resolve('User data'),
Promise.reject(new Error('Network error')),
Promise.resolve('Settings loaded'),
Promise.reject(new Error('Auth failed'))
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`Task ${i}: SUCCESS - ${result.value}`);
} else {
console.log(`Task ${i}: FAILED - ${result.reason.message}`);
}
});
const successes = results.filter(r => r.status === 'fulfilled');
console.log(`\n${successes.length}/${results.length} tasks succeeded`);
}
fetchAll();
BigInt handles numbers beyond Number.MAX_SAFE_INTEGER without precision loss. Note: === is false between BigInt and Number but == is true. Promise.allSettled never rejects - it waits for all promises and reports each outcome.
// Nullish coalescing assignment (??=)
const defaults = { theme: null, fontSize: undefined, lang: 'en' };
defaults.theme ??= 'dark'; // null -> assigns
defaults.fontSize ??= 16; // undefined -> assigns
defaults.lang ??= 'fr'; // 'en' -> keeps
console.log(defaults);
// Logical OR assignment (||=)
const options = { retries: 0, verbose: false, name: '' };
options.retries ||= 3; // 0 is falsy -> assigns (caution!)
options.verbose ||= true; // false is falsy -> assigns
options.name ||= 'anonymous'; // '' is falsy -> assigns
console.log(options);
// Logical AND assignment (&&=)
const user = { name: 'Alice', session: 'abc123' };
user.name &&= user.name.toUpperCase(); // truthy -> transforms
user.session &&= null; // truthy -> assigns null (logout)
user.nickname &&= 'Ali'; // undefined -> keeps
console.log(user);
// Practical: initializing nested defaults
const config = {};
config.database ??= {};
config.database.host ??= 'localhost';
config.database.port ??= 5432;
console.log(config);
??= only assigns when the left side is null/undefined. ||= assigns when falsy (be careful with 0 and ''). &&= only assigns when the left side is truthy - useful for conditional transformations. These operators are shorthand for common if-assign patterns.
// Array.at() - negative indexing
const fruits = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];
console.log('First:', fruits.at(0));
console.log('Last:', fruits.at(-1));
console.log('Second to last:', fruits.at(-2));
// Compare with old way:
console.log('Old way last:', fruits[fruits.length - 1]);
// Also works on strings
const str = 'Hello';
console.log('Last char:', str.at(-1));
// Object.hasOwn() - cleaner hasOwnProperty
const config = { debug: false, port: 3000 };
const inherited = Object.create(config);
inherited.host = 'localhost';
console.log('\n--- Object.hasOwn ---');
console.log(Object.hasOwn(inherited, 'host')); // own
console.log(Object.hasOwn(inherited, 'port')); // inherited
console.log(Object.hasOwn(inherited, 'debug')); // inherited
// Safe even with null-prototype objects
const bare = Object.create(null);
bare.key = 'value';
// bare.hasOwnProperty('key'); // TypeError!
console.log(Object.hasOwn(bare, 'key')); // works
// String.matchAll (ES2020)
const text = 'Price: $42.50, Tax: $3.40, Total: $45.90';
const regex = /\$(\d+\.\d+)/g;
console.log('\n--- matchAll ---');
for (const match of text.matchAll(regex)) {
console.log(`Found ${match[0]} at index ${match.index}, amount: ${match[1]}`);
}
// globalThis
console.log('\nglobalThis === global:', globalThis === global);
Array.at(-1) is cleaner than arr[arr.length - 1] for end-of-array access. Object.hasOwn() is safer than hasOwnProperty - it works on null-prototype objects and doesn't require call(). String.matchAll returns an iterator of all regex matches with capture groups.
optional chaining, nullish coalescing, BigInt, globalThis, Promise.allSettled, logical assignment, Array.at, Object.hasOwn