Skip to main content

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 блокує старт усього дерева залежностей.

Швидкий приклад

javascript
// 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-функції:

javascript
// Раніше це була помилка 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 на верхньому рівні модуля:

  1. Модуль стає асинхронним.
  2. Його виконання призупиняється, доки await не завершиться.
  3. Інші модулі, які імпортують цей модуль, теж чекають, доки він завершить виконання, через ланцюжок залежностей.

Приклад 1, імпорт з очікуванням:

javascript
// user.mjs export const user = await fetch('/api/user').then(r => r.json());
javascript
// main.mjs import { user } from './user.mjs'; console.log('Ім’я користувача:', user.name);

Тут main.mjs не почне виконуватися, доки user.mjs не завершить свій await.

Якщо модуль A використовує await на верхньому рівні, а модуль B імпортує A, то B теж чекає на завершення A:

javascript
// 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');

Вивід:

javascript
A start A done B start

Практичні приклади

Приклад 2, динамічна ініціалізація конфігу:

javascript
// 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 з динамічним імпортом:

javascript
// main.mjs const lang = navigator.language.startsWith('fr') ? 'fr' : 'en'; const messages = await import(`./messages.${lang}.js`); console.log(messages.default.hello);

Це дозволяє динамічно імпортувати модуль асинхронним синтаксисом, без зайвих обгорток.

Приклад у Node.js:

json
{ "type": "module" }
javascript
// 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 і мати запасне значення.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.