Why Should You Use Top-level Await in JavaScript?

Why Should You Use Top-level Await in JavaScript?

Subscribe to my newsletter and never miss my upcoming articles

Why Should You Use Top-level Await in JavaScript?

A feature that can change the way we JavaScript

JavaScript is a very flexible and powerful language that has transformed the web in this modern era. One of the main reasons for JavaScript to be dominant in web development is that it is continuously improved with major updates often.

One such proposal is concerning a new feature called “top-level await”. This particular feature enables ES modules to act as async functions. It allows ES modules to await resources and block other modules who import them. The modules who import an awaiting resource can only evaluate the body after the resources have been settled and are ready.

This proposal is currently in stage 3, therefore you can’t directly use it in production. But it is beneficial for you to learn it as it will soon be rolled out in the near future.

Don’t worry if it sounds alien. Go on. I’ve got you covered.

What was wrong with the pre-top-level era?

Before the introduction of top-level await, if you ever try to use the await keyword outside of an async function, you would receive a Syntax error. To avoid this, developers used Immediately Invoked Function Expressions(IIFEs).

But the above instance and solution is just the tip of the iceberg.

When you work with ES6 modules, there will be ample instances where you will be exporting values and importing them. Let’s look at such an example.

In the above example, we export and import variables between library.js and middleware.js . You can name the files in any way you want.

If you carefully have a look, you would notice a delay function that basically returns a Promise which would resolve after a timeout. Since it is asynchronous, we use the await keyword inside our async IIFE to wait until the execution completes. In a real-world example, the promise would be replaced by a fetch call or a task of asynchronous nature. Once the promise is resolved, we assign values to our variables which are computed from the functions imported from library.js . This basically means that our variables are undefined until the promise gets resolved.

When you have a look at the end of the above code snippet, you will see that the variables we have calculated are being exported, so that another module can use them.

Let’s look at a module that imports and uses the above-exported variables.

If you run the above code snippet, you will notice that the first two log statements are undefined and the latter print values 169 and 13. How does this happen?

This is because the exports from the module middleware.js are accessed by main.js before the completion of the async function. Remember we had a promise waiting to be resolved…

To solve this problem, we should somehow notify the modules importing these variables when it is ready to access them.

Workarounds

There are two commonly used workarounds available for the above problem.

1.Export a Promise to represent initialization

You can export the IIFE and can use that to identify when the exports are ready to be accessed. The async keyword makes a method asynchronous, which in turn always returns a promise. This is why the async IIFE returns a promise in the below solution.

When you access these exports from main.js , you can wait for the async IIFE to resolve and then access the variables.

Although the above solution does the job, it introduces some new problems.

  • Everyone should start following this pattern as a standard as you have to find the right promise to wait.
  • If another module depends on the variables squareOutput and diagonalOutput from main.js , we should make sure we re-export the IIFE promise as well. So that the other module too knows when to access our variables.

There is another workaround that solves the above-said issues.

2.Resolve the IIFE promise with the variables that should be exported

In this workaround, rather than exporting the variables separately, we return them from our async IIFE. This allows our main.js file to simply wait for the promise to resolve and retrieve the value.

But this solution too has its own set of complications.

According to the proposal, “this pattern has the undesirable effect of requiring a broad reorganization of the related source into more dynamic patterns and placing much of the module body inside the .then() callback in order to use the dynamically available imports. This represents a significant regression in terms of static analyzability, testability, ergonomics, and more, compared to ES2015 modules”.

How Top-level Await Addresses the Above Problem?

Top-level await allows us to let the module system take care of the promises and the coordination amongst them. This makes it really simple on our end.

None of the statements in main.js will execute until the await promises are resolved in middleware.js . This is a much cleaner solution than the mentioned workarounds.

Note

You must note that the top-level await only works for ES modules. The dependencies are required to be explicitly mentioned in order for this to work. The below example from the proposal repo explains this really well.

// x.mjs
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");</span><span id="8445" class="el km jl do ji b lh ll lm ln lo lp lj s lk">// y.mjs
console.log("Y");</span><span id="2a6f" class="el km jl do ji b lh ll lm ln lo lp lj s lk">// z.mjs
import "./x.mjs";
import "./y.mjs";</span><span id="cb10" class="el km jl do ji b lh ll lm ln lo lp lj s lk">//X1
//Y
//X2</span>

The above snippet will not print X1,X2,Y as expected. This is because x and y are adjacent modules that do not depend on each other.

I would highly suggest you read the FAQ of the docs to gain a thorough understanding of this awesome feature.

Implementation

V8

You can try out the above solution as mentioned in the docs.

I used the V8 method to try this out. Simply find out the file location of Chrome on your PC. Make sure all instances of Chrome are closed. Now open command prompt and run the below command in the folder where Chrome is installed.

chrome.exe --js-flags="--harmony-top-level-await"</span>

The above command will open Chrome with top-level await feature enabled.

You can try this on Node as well. Read this guide to know more.

ES Modules

Make sure you include type=module in your script tag.

<script type="module" src="./index.js" >
</script></span>

Moreover, you must note that unlike regular scripts, ES6 modules are subject to CORS policy. Therefore you have to serve them from a server.

I used XAMPP Apache for my testing purposes.

Use Cases

According to the proposal, these are the use cases.

Dynamic dependency pathing

const strings = await import(`/i18n/${navigator.language}`);</span>

This allows modules to use runtime values to evaluate the dependencies. This is useful for things like development/production splits, internationalization, environment splits, etc.

Resource initialization

const connection = await dbConnector();</span>

This helps the modules to represent resources and also to create errors in cases where the module can not be used. This can help with a fallback solution as shown below.

Dependency fallbacks

The below examples show how top-level await can be used to load dependencies with a fallback implementation. If the import from CDN A fails, the below code using top-level await can import jQuery from an alternate CDN B.

let jQuery;
try {
  jQuery = await import('https://cdn-a.example.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.example.com/jQuery');
}</span>

Criticisms

Rich Harris compiled a list of criticisms regarding the top-level await feature. This is what it contained.

  • Top-level await could block the execution of code.
  • Top-level await could block the fetching of resources.
  • There would be no clear interop story for CommonJS modules.

The proposal’s FAQ directly addresses these criticisms.

  • As siblings are able to execute, there is no definitive blocking.
  • Top-level await occurs during the execution phase of the module graph. At this point, all resources have already been fetched and linked. There is no risk of blocking fetching resources.
  • Top-level await is limited to modules. There is explicitly no support for scripts or for CommonJS modules.

I would highly suggest you read the FAQ of the proposal to deepen your understanding of this feature.

I hope you got a great understanding of this awesome new feature. How eager are you to try out this cool feature? Comment below.

Share your reusable components between projects using Bit

Bit (GitHub) makes it simple to share, document, and organize independent components from any project.

Use it to maximize code reuse, collaborate on independent components, and build apps that scale.

Bit supports Node, TypeScript, React, Vue, Angular, and more.

[Image for post

Image for post](bit.dev) Exploring components shared on Bit.dev [## The shared component cloud

Bit is a scalable and collaborative way to build and reuse components. It's everything you need from local development…

bit.dev](bit.dev)

Learn More

[## Build Scalable React Apps by Sharing UIs and Hooks

How to build scalable React apps with independent and shareable UI components and hooks.

blog.bitsrc.io](/build-scalable-react-apps-by-sharing-uis-and-hooks-fa2491e48357) [## How We Build Micro Frontends

Building micro-frontends to speed up and scale our web development process.

blog.bitsrc.io](/how-we-build-micro-front-ends-d3eeeac0acfc) [## Maximizing Code Reuse in React

How to speed-up development by sharing ReactJS components from any codebase, using Bit

blog.bitsrc.io](/maximizing-code-reuse-in-react-35ee20ad362c)

Resources Spec Proposal V8 Blog

 
Share this
Proudly part of