ES2020 Features
ES2020 (the 11th edition of ECMAScript) shipped some of the most widely used features in modern JavaScript. Two of them — optional chaining (?.) and nullish coalescing (??) — quietly eliminated entire categories of defensive boilerplate. Alongside them came BigInt for arbitrary-precision integers, Promise.allSettled for resilient concurrency, a portable globalThis, and standardized dynamic import(). This page walks through each feature with runnable examples for both the browser and Node.
Optional chaining (?.)
Optional chaining lets you safely read deeply nested properties without manually checking each link in the chain. If any reference before ?. is null or undefined, the whole expression short-circuits and evaluates to undefined instead of throwing a TypeError.
const user = {
name: "Ada",
address: { city: "London" },
};
// Before ES2020
const zip1 = user && user.address && user.address.zip;
// With optional chaining
const zip2 = user?.address?.zip;
console.log(zip1, zip2);
Output:
undefined undefined
It works on three call forms: property access (obj?.prop), dynamic access (obj?.[key]), and function/method calls (obj.method?.()). The call form is handy when a callback may or may not be provided.
function notify(onDone) {
// Only invokes onDone if it was actually passed.
onDone?.("complete");
}
notify(); // no error
notify((s) => console.log(s)); // logs "complete"
Tip:
?.only guards againstnull/undefined. It does not suppress errors thrown inside a method you call — it only checks whether the thing before?.is nullish before continuing.
Nullish coalescing (??)
The nullish coalescing operator returns its right-hand side only when the left-hand side is null or undefined. This is the key difference from ||, which falls back on any falsy value — including 0, "", and false, which are often valid data.
const config = { volume: 0, label: "" };
console.log(config.volume || 50); // 50 — wrong, 0 is valid!
console.log(config.volume ?? 50); // 0 — correct
console.log(config.label ?? "n/a"); // "" — preserves empty string
| Left value | x || y | x ?? y |
| --- | --- | --- |
| null / undefined | y | y |
| 0 | y | 0 |
| "" | y | "" |
| false | y | false |
| "hi" | "hi" | "hi" |
?. and ?? pair beautifully for reading config with sensible defaults:
const timeout = settings?.network?.timeout ?? 3000;
Warning: You cannot mix
??with&&or||without parentheses —a ?? b || cis a syntax error. Write(a ?? b) || cto make precedence explicit.
BigInt
BigInt represents integers beyond the safe range of Number (2^53 - 1). Create one with the n suffix or the BigInt() function. BigInts and regular numbers cannot be mixed in arithmetic — you must convert explicitly.
const big = 9007199254740991n + 2n;
console.log(big); // 9007199254740993n
console.log(typeof big); // "bigint"
console.log(BigInt(42) === 42n); // true
// console.log(1n + 1); // TypeError: cannot mix BigInt and other types
Output:
9007199254740993n
bigint
true
BigInt is ideal for database IDs, timestamps in nanoseconds, and cryptography. Note that it does not support fractional values and is not serializable by JSON.stringify without a custom replacer.
Promise.allSettled
Promise.all rejects as soon as any input promise rejects, discarding the results of the others. Promise.allSettled instead waits for every promise to finish and reports each outcome, making it the right tool when partial failure is acceptable.
const results = await Promise.allSettled([
Promise.resolve("ok"),
Promise.reject(new Error("boom")),
fetch("/api/data").then((r) => r.status),
]);
for (const r of results) {
if (r.status === "fulfilled") console.log("value:", r.value);
else console.log("reason:", r.reason.message);
}
Each entry is either { status: "fulfilled", value } or { status: "rejected", reason }.
globalThis
Before ES2020 the global object had a different name everywhere: window in browsers, self in Web Workers, and global in Node.js. globalThis provides a single, portable reference that works in all of them.
globalThis.appVersion = "1.4.0";
console.log(globalThis.appVersion); // works in browser, worker, and Node
Dynamic import()
Static import statements are hoisted and run at module load time. The dynamic import() form is a function-like expression that returns a promise, letting you load modules on demand — for code splitting, conditional loading, or lazy features.
button.addEventListener("click", async () => {
const { renderChart } = await import("./chart.js");
renderChart(data);
});
Because it returns a promise, import() also works in non-module contexts and pairs naturally with await.
String.prototype.matchAll
matchAll returns an iterator over all matches of a global regex, including capture groups — far cleaner than looping with regex.exec. The regex must have the g flag.
const text = "id=42, id=99, id=7";
const matches = [...text.matchAll(/id=(\d+)/g)];
console.log(matches.map((m) => m[1]));
Output:
[ '42', '99', '7' ]
Module namespace re-exports
ES2020 added the export * as ns from "module" syntax, letting you re-export another module’s entire namespace under a single name in one line.
// utils/index.js
export * as math from "./math.js";
export * as strings from "./strings.js";
// consumer.js
import { math } from "./utils/index.js";
console.log(math.add(2, 3));
Best Practices
- Reach for
?.to read uncertain nested data, but don’t overuse it — chaining through values that should always exist hides real bugs. - Prefer
??over||whenever0,"", orfalseare legitimate inputs. - Always parenthesize when combining
??with&&/||; the language forbids the ambiguous form on purpose. - Use
Promise.allSettledfor fan-out work where one failure shouldn’t sink the rest; keepPromise.allfor all-or-nothing operations. - Reach for
BigIntonly when values genuinely exceedNumber.MAX_SAFE_INTEGER— it’s slower and can’t mix withNumber. - Use dynamic
import()for code splitting and rarely used features to shrink your initial bundle. - Write
globalThisinstead ofwindow/globalfor code that must run across browser, worker, and Node environments.