Top-level await
Top-level await це можливість використовувати await безпосередньо на верхньому рівні ES-модуля, поза async-функцією. Модуль із таким await сам стає асинхронним: його завантаження призупиняється, доки проміс не виконається.
Теорія
TL;DR
- Раніше
awaitдозволявся тільки всерединіasync-функції, тому для ініціалізації писали обгорткиinit()чиbootstrap(). - Тепер
awaitможна писати на верхньому рівні модуля, і модуль просто «чекає» на цьому рядку. - Працює лише в ES-модулях:
.mjs,.jsз"type": "module"уpackage.json,<script type="module">. У CommonJS недоступно. - Модуль, що використовує top-level await, при імпорті фактично повертає проміс, тому всі, хто його імпортує, теж чекають.
- Основне застосування: асинхронна ініціалізація, читання конфігів, динамічні імпорти
await import(...). - Головний ризик: один довгий
awaitблокує старт усього дерева залежностей.
Швидкий приклад
// module.mjs або файл у проєкті з type: "module"
const response = await fetch('https://api.example.com/user');
const user = await response.json();
console.log('Користувач:', user.name);Код призупиняє завантаження модуля, доки проміс не виконається. Саме це й робить модуль асинхронним за своєю природою.
Як було раніше
До top-level await використовувати await можна було тільки всередині async-функції:
// Раніше це була помилка
const data = await fetch('/api/data');
// А так працювало
async function main() {
const data = await fetch('/api/data');
console.log(await data.json());
}
main();Якщо треба було дочекатися чогось на початку модуля, доводилося створювати функцію-обгортку (init(), bootstrap() і подібні) і не забувати її викликати.
Де можна використовувати
Тільки в ES-модулях (ESM), тобто у файлах:
- з розширенням
.mjs, або - у
.js, якщо вpackage.jsonвказано"type": "module", або - у
<script type="module">у браузері.
У CommonJS (Node.js із require) це неможливо.
Як це працює під капотом і як впливає на інші модулі
Коли збирач або рантайм зустрічає await на верхньому рівні модуля:
- Модуль стає асинхронним.
- Його виконання призупиняється, доки
awaitне завершиться. - Інші модулі, які імпортують цей модуль, теж чекають, доки він завершить виконання, через ланцюжок залежностей.
Приклад 1, імпорт з очікуванням:
// user.mjs
export const user = await fetch('/api/user').then(r => r.json());// main.mjs
import { user } from './user.mjs';
console.log('Ім’я користувача:', user.name);Тут main.mjs не почне виконуватися, доки user.mjs не завершить свій await.
Якщо модуль A використовує await на верхньому рівні, а модуль B імпортує A, то B теж чекає на завершення A:
// a.mjs
console.log('A start');
await new Promise(r => setTimeout(r, 1000));
console.log('A done');
// b.mjs
import './a.mjs';
console.log('B start');Вивід:
A start
A done
B startПрактичні приклади
Приклад 2, динамічна ініціалізація конфігу:
// config.mjs
const env = await fetch('/env.json').then(r => r.json());
export const API_URL = env.production ? 'https://api.example.com' : 'https://api.staging.example.com';Модуль config тепер можна імпортувати як звичайний, а він сам асинхронно підтягне конфігурацію.
Приклад 3, top-level await з динамічним імпортом:
// main.mjs
const lang = navigator.language.startsWith('fr') ? 'fr' : 'en';
const messages = await import(`./messages.${lang}.js`);
console.log(messages.default.hello);Це дозволяє динамічно імпортувати модуль асинхронним синтаксисом, без зайвих обгорток.
Приклад у Node.js:
{
"type": "module"
}// index.js
import fs from 'fs/promises';
const config = JSON.parse(await fs.readFile('./config.json', 'utf-8'));
console.log('Config:', config);Це працює, бо Node.js підтримує top-level await в ESM-режимі.
Переваги, ризики і підсумок
Переваги:
- Спрощує ініціалізацію модулів, немає потреби в
async init()навколо коду. - Прибирає «піраміду імпортів» для асинхронних залежностей.
- Дозволяє гнучко робити асинхронні імпорти через
await import().
Ризики:
- Блокування ланцюжка модулів: якщо один модуль «висить» на довгому
await, усе дерево залежностей чекає на його завершення. - Недоступність у CommonJS: тільки ESM.
- Може вплинути на час старту застосунку, особливо в Node.js.
| Особливість | Опис |
|---|---|
| Що робить | Дозволяє використовувати await поза async-функціями |
| Де працює | Тільки в ES-модулях |
| Що повертає | Модуль стає асинхронним, при імпорті повертає проміс |
| Застосування | Асинхронна ініціалізація, динамічні імпорти |
| Ризики | Блокування завантаження модулів, недоступність у CommonJS |
Типові помилки
- Намагатися використати top-level await у CommonJS. У файлі без
"type": "module"або без розширення.mjsце синтаксична помилка; спершу треба перевести проєкт чи файл на ESM. - Забувати про
<script type="module">. У звичайному<script>top-level await не працює, бо це не модуль. - Робити довгі мережеві запити на верхньому рівні часто імпортованого модуля. Такий
awaitзатримує старт усіх, хто його імпортує, і застосунок довго «нічого не показує». Краще винести це в явну функцію ініціалізації або віддавати проміс. - Вважати, що top-level await робить імпорти паралельними. Модулі в ланцюжку чекають один на одного; для паралельних запитів усередині одного модуля потрібен
Promise.all. - Створювати циклічні залежності з top-level await. Якщо два модулі чекають один на одного, виконання не завершиться взагалі.
- Не обробляти помилку. Відхилений проміс на верхньому рівні провалює завантаження всього модуля, тому критичні місця варто загортати у
try...catchі мати запасне значення.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.