Streamline your JavaScript code with top-level await

Prevent your JavaScript code from getting hung up or returning errors while waiting for external resources or long-running processes.
74 readers like this.
JavaScript in Vim

Alex Sanchez. CC BY-SA 4.0.

JavaScript is a popular programming language that started out as a single-threaded, synchronous language, meaning that one step runs after another step without waiting to fetch any external resources or perform any lengthy calculation or process. If the script requires such a resource or calculation, this synchronous behavior results in an error. This blocks all other processes in the queue from running whether or not they were dependent on the step that produced the error.

But some time ago, JavaScript introduced a new feature that made it possible to wait only for code that requires an external resource to load or a lengthy process to complete while processing and rendering the rest of the code. This asynchronous behavior is achieved by using callbacks or promises, which work at the function level.

What are callbacks and promises?

I'll explain these concepts through the help of code. If you already know what callbacks and promises are, feel free to skip down to the top-level await section and example application.

Callbacks

In a callback, one function is passed to another function as an argument; therefore, the second argument in the addEventListener function below is a callback function. This callback will wait for the first argument to happen and—only then—execute the body of the second argument.

const x = document.getElementsByTagName('Button');
x[0].addEventListener('click',() =>{alert("I was clicked")})

This waiting behavior makes the code asynchronous. This is unlike synchronous code, where tasks are performed one after another without waiting for a resource to download or a lengthy calculation to process. Note, however, that not all callback functions are asynchronous.

Promises

Promises are similar to callbacks in the sense that they attach a function to the returning object. However, there are differences between callbacks and promises. Promises are designed mainly for asynchronous functions. They only have one argument, and a .then() function is chained after the results once its only argument returns. Also, there can be multiple .then() and catch() functions attached to it.

fetch('www.xyz.com/api')
.then((res)=>{let x = res.data; //do something with received data})
.catch((err)=>{console.log(err)})

Promises use an event queue and strictly follow the order in which the tasks are chained.

Async/await

Async/await is the syntactical modification of promises to avoid chaining. It makes the code a lot cleaner and easier to understand. The await keyword makes the code halt until the promise is resolved or rejected.

async function asyncwaitcode(){
  let getData = await axios('www.xyzdata.org/api')
  console.log(getData.data)
} 

What is top-level await?

All of the examples above make the steps in functional blocks asynchronous, and none works at the modular level.

However, asynchronous behavior can be achieved at the modular level. The purpose of using top-level await is to allow modules to asynchronously initialize their namespace before notifying the consumer that the module has finished evaluating.

The following example app uses top-level await to show how this works.

About the app

This app will fetch the top news data from a news API and render it locally in the browser. This app also has search functionality so that users can get news data about a searched term. Before I start, there are few caveats to know:

  • Top-level await is supported in node version 13.3 and above, and none of these versions has LTS (long-term support) at this time.
  • Top-level await is supported only in the ECMAScript module, and Node.js and Express use CommonJS modules. However, support for the ECMAScript module has been added in Node.js. So, instead of using require to fetch the modules, I use import in the entire app. CommonJS does not support top-level await.
  • Top-level await doesn't work with named export; it only works with default exports.
  • In versions prior to Node.js 14.x, top-level await doesn't run out of the box. Instead, you have to use Google V8 engine's --harmony top-level await flag to run it. However, it is fully supported in 14.x and above (i.e., it runs without flags).
  • Top-level await does not work with classic scripts and non-asynchronous functions.
  • Circular module dependencies could introduce a deadlock.

Prerequisites

  • Node.js version 13.3 or above
  • npm
  • API keys for the news APIs (see below)

Build the app

  1. First, make a new directory named toplevelawait:

    $ mkdir toplevelawait
  2. Run npm init to create a package.json file:

    $ npm init
  3. Add "type": "module" to package.json to add support for ECMAScript modules:

    "author": "",
    "license": "ISC",
    "type": "module",
  4. Make a src folder in the toplevelawait folder. Inside the src folder, make a views directory to save the embedded JavaScript (.ejs) files.

    $ mkdir -p src/views
  5. Make two files—app.mjs and exp.mjs—in the src folder. Note the file extension is .mjs and not just .js, which indicates that you are using ECMA modules.

    $ touch app.mjs
    $ touch exp.mjs
    $ ls -1 src
    app.mjs
    exp.mjs
    
  6. Next, add the dependencies axios, ejs, and express:

    $ npm install axios ejs express --save
  7. Add the following in the exp.mjs file:

    import express from "express"
    
    export const exp = await express();

Note that this uses the await keyword without the async keyword. This wait is waiting for the express instance to initialize before exporting it to other modules. You can use this example to wait for the instances to initialize before moving to code that depends on the awaited resource.

If a module contains a top-level await, then this module's and the parent module's execution will halt until the promise is resolved. All the siblings will continue executing in the usual, synchronous way.

Note that module loading in Node.js is also synchronous, which means it doesn't wait for a resource to load. You will get an error. However, you can achieve asynchronous behavior by placing the await keyword in front of the resource that is loading or doing some processing.

Add the news APIs

This app uses two freely available news APIs to get data. Using two APIs supports fallback dependency behavior; if one API fails to retrieve the data, the other will get it. Both of these APIs use API keys:

Insert the following code in the app.mjs file. This first section imports the axios and express instances initialized in exp.js. The next part sets the view engine to view .ejs files in a browser:

import { exp } from "./exp.mjs";
import axios from "axios"

exp.set("view engine","ejs");
// dependency fall back
let response = "";
let site = true;
try{
   response = await axios('https://newsapi.org/v2/top-headlines?country=us&apiKey=your-api-key');  
 }
 catch{
  response = await axios("https://gnews.io/api/v3/top-news?token=your-api-key"); 
  site = false; 
 }
 // Get top news
exp.get('/',function(req,res){
  let response0 = response.data.articles  
  res.render('main.ejs',{response0: response0, site:site})
 })
 // search news
exp.get('/search', function(req,res){
  res.render("searchnews.ejs")   
})
exp.get('/result', async(req, res)=>{
  let x = req.query.newtitlesearch;
  let response1 = {}
  let data = {}
  try{
    let url = 'https://newsapi.org/v2/everything?q='+x+'&apiKey=your-api-key'
    response1 =  await axios(url);
  }
  catch{
    let url = 'https://gnews.io/api/v3/search?q='+x+'&token=your-api-key'
    response1 =  await axios(url)
  }
  res.render('result.ejs', {response1: response1.data.articles, site: site})  
})
exp.listen(3000)

The most important part is next: the try and catch block, which uses top-level await to wait for axios to get the data from the API and, if for any reason, axios fails to retrieve the data from the first API, the app uses the second API to get the data. Once it gets the data from the API, express renders it on the main page:

try{
   response = await axios('https://newsapi.org/v2/top-headlines?country=us&apiKey=your-api-key');
  
 }
 catch{
  response = await axios("https://gnews.io/api/v3/top-news?token=your-api-key");
  
 }

After that is another express route that takes users to a search form where they can search for news they are interested in:

// search news
exp.get('/search', function(req,res){
  res.render("../src/view/searchnews.ejs")  
})

Finally, another express route displays results from the search:

exp.get('/result', async(req,res)=>{
  let x = req.query.newtitlesearch;
  let response1 = {}
  let data = {}
  try{
    let url = 'https://newsapi.org/v2/everything?q='+x+'&apiKey=your-api-key'
    response1 =  await axios(url);
  }
  catch{
    let url = 'https://gnews.io/api/v3/search?q='+x+'&token=your-api-key'
    response1 =  await axios(url)
  }
  res.render('../src/view/result.ejs', {response1: response1.data.articles , site: site})  
})

Write the frontend pages

The last part of the app writes the four .ejs HTML files for the frontend pages. Save these files in the views folder:

//header.ejs
<!DOCTYPE html>
<head>
    <title>newapiapp</title>
    <link type="text/css" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" >
</head>

<body>  
    <nav class="navbar navbar-default">
              <div class="container-fluid">
                  <div class="navbar-header">
                      <a class="navbar-brand" href="#">News app</a>
                  </div> 
                  <div class="collapse navbar-collapse">
                      <ul class="nav navbar-nav navbar-right">
                             <li><a href="https://opensource.com/">Main</a></li>                
                              <li><a href="https://opensource.com/search">Search</a></li>      
                      </ul>      
                  </div>
              </div>  
    </nav>
//main.ejs
<%- include('header');%>
<%let rows = response0%>
<%let siterep = site%>

<div name "container">
  <div class="row text-center" style="display:flex; flex-wrap:wrap">
    <% for(let i = 0; i < rows.length; i++){%>
      <div class="col-md-3 col-sm-6 ">
                          <div class="thumbnail" >
                            <a href="https://opensource.com/%3C%25-rows%5Bi%5D.url%20%25%3E">
                              <img src = "<%= siterep ? rows[i].urlToImage :  rows[i].url  %>">
                            </a>                            
                          </div>
                          <div><%= rows[i].title%></div>                 
                        </div>    
    <% } %>
  </div>  
</div>
//Searchnews.ejs 
<%- include('header');%>

  <h1>Search news </h1>
  <form action="https://opensource.com/result" method="Get">
      <iput type ="text" placeholder="news title search" name="newtitlesearch"></input>
        <input type="submit" placeholder="submit"></input>        
   </form>
//result.ejs
<%- include('header');%>
<%let rows = response1%>
<%let siterep = site%>

<div name "container">
  <div class="row text-center" style="display:flex; flex-wrap:wrap">

    <% for(let i = 0; i < rows.length; i++){%>
      <div class="col-md-3 col-sm-6 ">
                          <div class="thumbnail" >
                            <a href="https://opensource.com/%3C%25-rows%5Bi%5D.url%20%25%3E">
                              <img src = "<%= siterep ? rows[i].urlToImage :  rows[i].url  %>">
                            </a>                            
                          </div>
                          <div><%= rows[i].title%></div>                  
                        </div>    
    <% } %>
  </div>  
</div>

Run the app

Now the app is complete, and you can try it out.

If you are using a Node.js version from v13.3 to v14.0, run the program with:

$ node --harmony-top-level-await app.js

If you are using Node.js v14 and up, you don't need to use V8's --harmony flag:

$ node app.js

If you have successfully built this app, congratulations! You have learned a new JavaScript feature.

You can find more uses of top-level await in the ECMAScript TC39 top-level await proposal.


The code for this top-level await example using Node.js by Sumaira Ahmad is open source under a CC BY 4.0 license.

What to read next
Tags
Avatar
I am a MERN stack developer and a machine learning enthusiast. I like to work on open source projects of my interest.

Comments are closed.

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.