blog post

Mastering Async Await in JavaScript for Asynchronous Programming

Introduction

Asynchronous programming is a must-have in modern JavaScript development, allowing developers to perform non-blocking operations, such as fetching data from a server, reading files, or executing time-consuming operations. ES2017 introduced async functions and the await keyword that are a complete game changer in asynchronous development. This blog post is a guide to using async/await to handle asynchronous tasks in an elegant way.

How To Use Async/Await

async/await statements allow developers to write asynchronous code that looks and behaves a like synchronous code:

js
async function fetchDataAsync() { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); return data; } const data = await fetchDataAsync(); console.log(data);

Here an async function returns a Promise<T>, that holds a data received from the API call. By using await keyword we get this data as a promise result.

After the line const data = await fetchDataAsync(); we can simply write more code as if all operations were executed synchronously.

async/await offer an elegant way for executing asynchronous operations represented by JavaScript Promises.

To learn more about promises read my blog post.

await keyword is only allowed to be used in the async functions.

js
function test() { const data = await fetchDataAsync(); // Syntax error } async function test() { const data = await fetchDataAsync(); // Now ok }

Async/Await in Top-Level Statements

With the introduction of ECMAScript 2022, JavaScript supports top-level await statements in modules. This allows you to use await outside of async functions within modules, simplifying the initialization of resources.

js
const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); console.log(data);

Outside modules or in older versions of web browsers, you can use the following trick with anonymous async function to use await in top-level statements:

js
(async () => { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); console.log(data); })();

Async Methods as Class Members

You can encapsulate asynchronous logic within objects by defining async methods in JS classes. It is a good practise to add Async suffix when naming asynchronous functions.

js
class PostService { async getPostsAsync() { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); return data; } } const postService = new PostService(); const data = await postService.getPostsAsync(); console.log(data);

Error Handing When Using Async/Await

Error handling when using async/await is straightforward by using try/catch statement:

js
async function fetchDataAsync() { try { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); return data; } catch (error) { console.error("Failed to fetch data: ", error); } }

When a reject method is called while awaiting a promise - an exception is thrown, that can be handled in the catch block:

js
function testPromise() { return new Promise((resolve, reject) => { setTimeout(() => { reject(new Error("Test Error")); }, 1000); }); } try { const response = await testPromise(); } catch (error) { console.error("Failed to get result: ", error); }

Using Promise Utility Methods With Async/Await

Promise class has few utility static methods for asynchronous programming:

  • Promise.all
  • Promise.any
  • Promise.race
  • Promise.allSettled

Promise.all

You can use Promise.all function to wait for multiple promises to resolve. This function takes an array of promises and returns a new promise that resolves when all of the promises have resolved, or rejects if any promise is rejected.

This method is particularly useful when you have multiple asynchronous operations that can be executed in parallel. Let's explore an example, where we fetch posts, comments and todos using Promise.all:

js
async function fetchMultipleResourcesAsync() { const urls = [ "https://jsonplaceholder.typicode.com/posts", "https://jsonplaceholder.typicode.com/comments", "https://jsonplaceholder.typicode.com/todos" ]; try { const promises = urls.map(url => fetch(url)); const responses = await Promise.all(promises); const data = await Promise.all(responses.map(res => res.json())); return data; } catch (error) { console.error("Error fetching one or more resources:", error); } return null; } const data = await fetchMultipleResourcesAsync(); console.log("Posts, comments, todos:", data);

It will be more efficient to fetch this data in parallel than fetching posts, comments and todos one by one.

Promise.any

You can use Promise.any function to wait for one of multiple promises to resolve. This function takes an array of promises and returns a single promise that resolves when the first of the promises is resolved. If all the promises are rejected, then the returned promise is rejected with an AggregateError, an exception type that groups together individual errors.

js
async function fetchFirstResourceAsync() { const urls = [ "https://jsonplaceholder.typicode.com/posts", "https://jsonplaceholder.typicode.com/comments", "https://jsonplaceholder.typicode.com/todos" ]; try { const promises = urls.map(url => fetch(url)); const firstResponse = await Promise.any(promises); const data = await firstResponse.json(); return data; } catch (error) { console.error("All requests failed:", error); } return null; } const data = await fetchFirstResourceAsync(); console.log("First available data:", data);

Promise.race

Promise.race is similar to Promise.any, but it completes as soon as one of the promises is either resolved or rejected. This method is useful for timeout patterns when you need to cancel request after a certain time.

js
async function fetchDataWithTimeoutAsync() { const fetchPromise = fetch("https://jsonplaceholder.typicode.com/comments"); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Request timed out")), 5000)); try { const response = await Promise.race([fetchPromise, timeoutPromise]); const data = await response.json(); return data; } catch (error) { console.error("Failed to fetch or timeout reached:", error); } return null; } const data = await fetchDataWithTimeoutAsync(); console.log("Comments received:", data);

Promise.allSettled

You can use Promise.allSettled function to wait for all the promises to complete, regardless of whether they resolve or reject. It returns a promise that resolves after all the given promises have either resolved or rejected. This promise contains an array of objects where each describes the result of each promise.

js
async function fetchMultipleResourcesAsync() { const urls = [ "https://jsonplaceholder.typicode.com/posts", "https://jsonplaceholder.typicode.com/comments", "https://jsonplaceholder.typicode.com/todos" ]; try { const promises = urls.map(url => fetch(url)); const results = await Promise.allSettled(promises); const data = results.map((result, index) => { if (result.status === "fulfilled") { console.log(`Promise ${index} fulfilled with data:`); return result.value; // Collecting fulfilled results } else { console.error(`Promise ${index} rejected with reason:`, result.reason); return null; // You might want to return null or a default object } }); return data; } catch (error) { console.error("Error fetching one or more resources:", error); } return null; } const data = await fetchMultipleResourcesAsync(); console.log("Posts, comments, todos:", data);

Awaiting Thenable Objects

In JavaScript, a thenable is an object or function that defines a then method. This method behaves similarly to the then method found in native promises. That way async/await can handle these objects just like regular promises.

For example:

js
class Thenable { then(resolve, reject) { setTimeout(() => resolve("Task completed"), 1000); } } const result = await new Thenable(); console.log(result);

Thenables objects can be useful for integrating with systems that don't use native promises but have promise-like behavior. However, thenable objects can introduce confusion to the source code as their behavior is not straightforward.

Native promises are preferable due to their comprehensive feature set and better integration with the JavaScript ecosystem.

Hope you find this blog post useful. Happy coding!

Improve Your .NET and Architecture Skills

Join my community of 1800+ developers and architects.

Each week you will get 2 practical tips with best practises and architecture advice.