JavaScript · ES2022

ES13 Interactive Hub — ECMAScript 2022

Top-level await, class fields (private + static), Array.at() negative index, Error cause, Object.hasOwn, class static blocks, ergonomic brand checks (#field in obj) — more class features + better DX.

Experience
0 XP
Course completion0%

Top-level await

Core topic

Hard6 min

💡 Concept summary

Use await directly at the module level — no need to wrap it in an async function. ONLY valid inside an ES module.

🤔 Why do you need this feature?

Before ES13, awaiting at the top level required wrapping it in an async IIFE — cumbersome. Especially when you need to initialize async data before exporting. Top-level await lets a module wait for resources before it is ready.

Before (old style)Verbose / clunky
// Before ES13 — had to wrap in an async IIFE
// file: config.mjs
let config;
(async () => {
    const res = await fetch('/api/config');
    config = await res.json();
})();
export { config }; // may not have finished loading yet!

// Or export a Promise
export const configPromise = fetch('/api/config').then(r => r.json());
ES13 (Modern)Optimal & recommended
// ES13 — top-level await in an ES module
// file: config.mjs
const res = await fetch('/api/config');
export const config = await res.json();

// Another file imports — this module will finish before running
// import { config } from './config.mjs';
// console.log(config); // data is already available

// Conditional dynamic import at the top level
const lang = navigator.language.startsWith('vi') ? 'vi' : 'en';
const { messages } = await import(`./locales/${lang}.js`);
export { messages };
Got it down? Try running real code or take the quiz!

ES13 Quick Reference

Quick syntax overview + copy snippets

Top-level await

Use await directly at the module level — no need to wrap it in an async function. ONLY valid inside an ES module.

const res = await fetch('/api/config');
export const config = await res.json();
const lang = navigator.language.startsWith('vi') ? 'vi' : 'en';
...
Class Private Fields (#)

Declare truly private fields with the "#" prefix — enforced by the browser, cannot be accessed from outside the class.

class User {
    name = '';       // public field
    #password = '';  // private field
...
Array.at() & String.at()

Access an element by index, supporting NEGATIVE indexes to count from the end — cleaner than arr[arr.length - 1].

const arr = [10, 20, 30, 40, 50];
console.log(arr.at(-1));  // 50 (last)
console.log(arr.at(-2));  // 40
...
Error cause

The { cause } argument when creating an Error retains the underlying error — useful when rethrowing at a higher layer to keep context.

async function loadDashboard() {
    try {
        await fetchUser();
...
Object.hasOwn()

A concise, safe replacement for "Object.prototype.hasOwnProperty.call(obj, key)" — checks whether a property belongs to the object or is inherited.

const obj = { name: "Lan" };
console.log(Object.hasOwn(obj, 'name'));     // true
console.log(Object.hasOwn(obj, 'toString')); // false (inherited)
...
Class static blocks

A "static { ... }" block runs ONCE as soon as the class is defined — used to initialize complex logic for static state (try/catch, accessing private fields, computation).

class Logger {
    static instances = [];
    static defaultLevel;
...
Ergonomic brand check (#field in obj)

The extended "in" operator lets you check whether an object has a private field using the "#field in obj" syntax — it does NOT throw an error like direct access does.

class Point {
    #x;
    #y;
...