What's new in ECMAScript 2020

ECMAScript 2020, the 11th installment of our favorite programming language, contains a handful of new features. Some are small ones, but others have the potential to change forever the way we write JavaScript.

This article is an attempt at a short and concise overview of those new features. Grab your cup of tea and let's go.

Dynamic import()

ES2015 introduced the static import syntax. Now you could export a variable from one module and then import it in another.

// utils.js
export function splitName(name) {
  return name.split(" ");
}

// index.js
import { splitName } from "./utils";

console.log(splitName("John Snow"));

That syntax is called static because you can't import a module dynamically (depending on some conditions) at runtime. Note that it's not necessarily a bad thing: static imports can be optimized at compile time, allowing for Tree Shaking.

Dynamic imports, on the other hand, when used wisely, can help with reducing bundle size by loading dependencies on-demand.

The new dynamic import syntax looks like a function (but it's not), and it returns a promise, which also means we can use async/await with it.

// ...
const mod = figure.kind === "rectangle" ? "rectangle.js" : "circle.js";
const { calcSquare } = await import(mod);
console.log(calcSquare(figure));

Nullish coalescing

The popular way of setting a default value with short-circuting has its flaws. Since it's not really checking for the emptiness, but rather checking for the falsyness, it breaks with values like false, or 0 (both of which are considered to be falsy).

ES2020 introduces a new operator ?? which works similarly but only evaluates to the right-hand when the initial value is either null or undefined.

Here's a quick example:

const initialVal = 0;

// old way
const myVar = initialVal || 10; // => 10

// new way
const myVar = initialVal ?? 10; // => 0

I wrote a detailed article about this feature and how it compares to the other methods for setting a default value.

Optional chaining

The new optional chaining operator aims to make the code shorter when dealing with nested objects and checking for possible undefineds.

const user = { name: "John" };

// Fails with `Uncaught TypeError: Cannot read property 'city' of undefined`
const city = user.address.city;

// Works but verbose
let city = "Not Set";
if (user.address !== undefined && user.address !== null) {
  city = user.address.city;
}

// Works and concise but requires a 3rd party library
const city = _.get(user, "address.city", "Not Set");

// 🤗
const city = user?.address?.city ?? "Not Set";

BigInt

BigInt is a new object that represents numbers higher than Number.MAX_SAFE_INTEGER (which is 2^53 - 1). While for normal folks, it may sound more than enough, for some math applications and machine learning, the new BigInt type comes in handy.

It comes with its own literal notation (just add an n to a number):

const x = 9007199254740991n;

// or it can be constructed from a string
const y = BigInt("9007199254740991234");

BigInts come with their own algebra, which is not translated to regular numbers by which we can't mix up numbers and BigInts. They should be coerced to either type first.

1 === 1n; // => false
1n + 1; // throws Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
6n << 3; // nope
6n << 3n; // that works

String.matchAll

So here's an example. Imagine, you have a very long string of text, and you need to extract all the tags (that are words starting with #) out of it. Regular expressions to the rescue!

const tweet = "#JavaScript is full of #surprises. Both good and bad ones #TIL";

for (h of tweet.matchAll(/(#\w+)/g)) {
  console.log(h[0]);
}

// or

const tags = [...tweet.matchAll(/(#\w+)/g)];

matchAll returns an iterator. We could either iterate over it with for..of, or we can convert it to an array.

Promise.allSettled

Remember the Promise.all function? It resolves only when all of the passed promises get resolved. It rejects if at least one of the promises got rejected, while the others may still be pending.

The new allSettled behaves differently. It resolves whenever all of the promises finished working, that is, became either fulfilled or rejected. It resolves to an array that contains both the status of the promise and what it resolved to (or an error).

Thus, allSettled is never rejected. It's either pending, or resolved.

A real-world problem might be removing a loading indicator:

// const urls = [...]
try {
  await Promise.all(urls.map(fetch))
} catch (e) {
  // at least one fetch is rejected here, but there may others still pending
  // so it may be too early for removing the loading indicator
  removeLoading()
}

// with allSettled
await Promise.allSettled(urls.map(fetch))
removeLoading()

globalThis

In JavaScript, there's always one big context object that contains everything. Traditionally, in browsers it was window. But if you try accessing it in Node application, you'll get an error. There is no window global object in Node; instead there is global object. Then again, in WebWorkers, there is no access to window, but there is self instead.

The new globalThis property abstracts away the difference. Meaning that you can always refer to globalThis without caring in which context you are now.

Now, if you think that the naming is rather awkward, I'm totally with you, but note that naming it self or global could make some older code incompatible. So I guess we'll have to live with that.

What's next?

For your convenience, here are the links to the MDN documentation for each of the features mentioned in this article.

If you like articles like this, you can follow me on Twitter to get notified about the new ones.

🔥 100+ questions with answers
🔥 50+ exercises with solutions
🔥 ECMAScript 2023
🔥 PDF & ePUB