ECMAScript: Top-level await


async/await

The async/await feature introduced in ECMAScript 2017 enables asynchronous, promise-based behavior to be written in a cleaner style avoiding the need for promise chains. The await keyword can only be used inside an async function. Attempting to use an await outside of an async function results in a SyntaxError - SyntaxError: await is only valid in async function.

The ECMAScript feature ‘Top-level await’ which is promoted to Stage 4 in the TC39 process lets us use the asynchronous await operator at the top level of modules. Top-level await enables modules to act as big async functions. With top-level await, ECMAScript Modules (ESM) can await resources. Other modules which import them have to wait before evaluating their code.

Sounds great, right?

Let us dive more into it.

As await is only available within async functions, a module can include an await in the code by wrapping the code in an async function.

  //a.mjs
  import fetch  from "node-fetch";
  let users;

  export const fetchUsers = async () => {
    const resp = await fetch('https://jsonplaceholder.typicode.com/users');
    users =  resp.json();
  }
  fetchUsers();

  export { users };

  //usingAwait.mjs

  import {users} from './a.mjs';
  console.log('All users: ', users); //user list
  console.log('In usingAwait module');

Immediately invoked top-level async function(IIAFE)

We can also use immediately invoked async function expression(IIAFE).

  import fetch  from "node-fetch";
  (async () => {
    const resp = await fetch('https://jsonplaceholder.typicode.com/users');
    users = resp.json();
  })();
  export { users };

But this has a downside. users is undefined directly after importing. We must wait until the asynchronous work is finished before we can access it.

  //usingAwait.mjs

  import {users} from './a.mjs';
  console.log('All users:', users); //undefined
  setTimeout(() => {
    console.log('All users:', users);  //user list
  }, 100);
  console.log('In usingAwait module');

This approach is not safe. It will not work if the async function takes longer than 100 milliseconds.

Export a Promise to represent initialization

Another approach is to export a promise letting the import module know that the data is ready.

//a.mjs
import fetch  from "node-fetch";
  export default (async () => {
    const resp = await fetch('https://jsonplaceholder.typicode.com/users');
    users = resp.json();
  })();
  export { users };

//usingAwait.mjs
import promise, {users} from './a.mjs';
  promise.then(() => { 
    console.log('In usingAwait module');
    setTimeout(() => console.log('All users:', users), 100); //user list
  });

Though this approach seems to give the desired result, it has some limitations.

  • Importing module must be aware of the pattern and should use it correctly.
  • If we forget to apply the pattern, we might get the desired result sometimes.

Top-level await

Top-level await eliminates all downsides of all the approaches we discussed.

  //a.mjs
  const resp = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = resp.json();
  export { users};

  //usingAwait.mjs
  import {users} from './a.mjs';

  console.log(users);
  console.log('In usingAwait module');

Module execution order

  • Modules follow the same post-order traversal as established in ES2015.
  • If a module reaches an await, it will yield control and let other modules initialize themselves in the same well-specified order.
  • Execution of the module starts with the deepest imports.
  • After a top-level await is reached, the control is passed to start the next module or to other asynchronously scheduled code.

Usage

Top-level await is very helpful for the below use cases -

    const strings = await import(`/i18n/${navigator.language}`);
  
    const connection = await dbConnector();
  
 
    let translations;
    try {
      translations = await import('https://app.fr.json');
    } catch {
      translations = await import('https://fallback.en.json');
    }
  

Implementation

Support for Top-level await -

Check out more details on Top-level await proposal and v8.dev.