4 Essential Tips for Better Asynchronous Code in JavaScript

Tips - Better Asynchronous JS Code

Learning new programming techniques is one of the best ways to take your code from "it works" to "good." Even if you have written an app or two, and know your way around asynchronous calls with callbacks, promises, and async/await, new tips for writing better asynchronous code will always come in handy.

We'll share a list of essential "Asynchronous programming 201" tips and tricks with examples that can help you beat single thread in JavaScript and improve your code efficiency. Let's pretend the history of asynchronous code started with Promises to avoid the "dawn of time/callback hell" intros and concentrate on the recent JavaScript asynchronous patterns.

For brevity, we'll assume each await is wrapped in an async function and fakeFetch() function is defined elsewhere.

1. Run In Parallel, Await Later to Make Your Code Run Faster

Promises are eager - it means the GET request is sent the moment we write fetchFile(file). On the other hand, await pauses the function execution until a promise is resolved. We can make our code efficient and faster by ditching the unnecessary awaiting, using eager promises, and requesting several files in parallel instead of one by one.

How to make parallel requests?

Just by creating a new Promise and getting the result later. In the code example below, we are fetching all three files concurrently and what we get are promises we need to resolve (await) somewhere later in code.

// fake fetch function returning a new Promise object
const fetchFile = (file) => new Promise(resolve => fakeFetch(file, resolve))

// request all files at once, in parallel
const promise1 = fetchFile('file1')
const promise2 = fetchFile('file2')
const promise3 = fetchFile('file3')

// await later
const file1 = await promise1

There is a convenient way to fetch multiple files simultaneously as the Promise object includes some useful methods for handling multiple promises at once. For example, Promise.all() requests files in parallel, and we can await the result in the same line. Instant gain!

// we can await for the value immediately, the files are still being requested in parallel
const fileArray = await Promise.all([fetchFile('file1'), fetchFile('file2'), fetchFile('file3')])

It is important to emphasize that too many parallel requests, especially when requesting large resources, can also lead to poor performance.

So, when do we use await?

When there is dependency between data in our code and we use the result of the previous promises resolved to proceed.

Let's say we need to get our files sequentially. In the code example below, fetch request for file2 is sent only after the promise returned from the first fetchFile() call resolves with file1. This code is much slower than the previous example of fetching files concurrently. Notice that the variables aren't referencing promises anymore, but files.

//request all files sequentially, in order
const file1 = await fetchFile('file1') // pause until resolved
const file2 = await fetchFile('file2') 
const file3 = await fetchFile('file3')

Here's an example of requests that should be made sequentially - to fetch user orders, we need to wait for the promise returned from fetchUser() to resolve with user data. This is where we need await.

const user = await fetchUser()
const userOrders = await fetchOrders(user)
  • Key takeaway tip: Use parallel requests for independent asynchronous tasks to avoid the pausing nature of await and the performance pitfall.

2. Create an Array of Promises With map()

In the examples above, we operate with a definite number of promises as we know in advance the exact number of files to fetch. How to fetch a collection of files with an arbitrary length?

When working with collections containing an indefinite number of items, our go-to tools in JavaScript are arrays and array methods. We can create an array of promises with map() and fetchFile() callback that returns a promise and maps fileNames array values to promises.

// fake fetch function returning a new Promise object
const fetchFile = (file) => new Promise(resolve => fakeFetch(file, resolve))

// array populated with an arbitrary number of elements
const fileNames = ['file1', 'file2', 'file3'/* ... */] 

// builds an array of promises with a callback that returns a Promise object
const promises = fileNames.map(fetchFile)

Once we have our promises packed into an array, we can do all kinds of fun stuff - loop over promises or pass it to any Promise object method that takes an iterable, such as Promise.all().

const files = await Promise.all(promises)
  • Key takeaway tip: When dealing with a whole lot of promises, store them into an array.

3. Loop Over Promises With for...of

The next step we might want to do is loop over the array of promises and do something with each promise sequentially, e.g. log the resolved values in order.

If we try to do it with the forEach() method, we will see that it doesn't work as expected.

// this will not log in order
promises.forEach(async promise => console.log(await promise))

Async callback always returns a promise, and the forEach() method (as well as other array methods such as map(), filter(), etc.) does not know what to do with promises. Being eager synchronous iterators, these methods do not know how to pause and wait for a value when they run into a promise. Therefore, in most cases, we shouldn't run asynchronous code inside the array methods callback.

What we need here is a for...of loop or even its asynchronous version for await...of which is the async iterator perfectly able to loop over sync iterables.

// for...of
for (const promise of promises) {
  console.log(await promise)
}

// for await...of
for await (const result of promises) {
  console.log(result)
}

// Promise.all returns a single promise that resolves with an array of results 
for (const result of await Promise.all(promises)) {
  console.log(result)
}
  • Key takeaway tips: Use array methods on arrays of promises. Avoid unpacking/awaiting for promises in callback functions. Go for for...of loop instead.

4. Consider Promise.allSettled() Instead Promise.all()

These two methods behave differently, and it's completely fine to use one or the other. But, before Promise.allSettled() was introduced in ES2020, developers were desperately trying to 'fix' Promise.all() behavior. Namely, Promise.all() would reject upon any of the promises rejecting, with the return value of the first rejected promise. What if we still want to get the values of the resolved promises?

A workaround solution is to add a custom catch() method to every promise in the input promise array. In the example below, the catch() method will set the value of the associated promise to 'rejected' in case the promise gets rejected.

const promises = fileNames.map(file => fetchFile(file).catch(() => 'rejected'))
const results = await Promise.all(promises)
const resolved = results.filter(result => result !== 'rejected')

With Promise.allSettled() we get an array of objects with the outcome of each promise out of the box. It will never reject - instead, it will wait for all promises passed in the array to either resolve or reject.

const promises = fileNames.map(fetchFile)
const results = await Promise.allSettled(promises)
const resolved = results.filter(result => result.status === 'fulfilled').map(result => result.value)
  • Key takeaway tips: UsePromise.all() to request in parallel and fail fast upon rejection, without waiting for all promises to resolve. Use Promise.allSettled() to request in parallel, never fail and get all the results.

Asynchronous JS Tips Summary

This list is not exhaustive or perfect. It's something that we, as a team, have learned from our experience of working with new asynchronous code patterns while making an effort to write clean and efficient code. Asynchronous JavaScript has come a long way in finding new ways to run operations without blocking the main thread - from callbacks through promises to async/await. There will surely be new developments and changes regarding the topic in the future, and hopefully, we'll be able to share some new useful tips.

Authors: ,