Skip to content
JavaScript js modern 5 min read

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 against null/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 || c is a syntax error. Write (a ?? b) || c to 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 || whenever 0, "", or false are legitimate inputs.
  • Always parenthesize when combining ?? with &&/||; the language forbids the ambiguous form on purpose.
  • Use Promise.allSettled for fan-out work where one failure shouldn’t sink the rest; keep Promise.all for all-or-nothing operations.
  • Reach for BigInt only when values genuinely exceed Number.MAX_SAFE_INTEGER — it’s slower and can’t mix with Number.
  • Use dynamic import() for code splitting and rarely used features to shrink your initial bundle.
  • Write globalThis instead of window/global for code that must run across browser, worker, and Node environments.
Last updated June 1, 2026
Was this helpful?