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.
Top-level await
Core topic
💡 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 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 — 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 };ES13 Quick Reference
Quick syntax overview + copy snippets
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';
...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
...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 ...
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();
...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)
...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;
...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;
...