Web Development

Asynchronous Operations in JavaScript

September 26th, 2019 | By Camilo Reyes | 4 min read

JavaScript comes from a legacy of asynchronous operations. It began with callbacks to make Ajax calls for partial page updates.

The humble callback function worked but had gotchas like callback hell. Since then, JavaScript has evolved into a modern language with Promises and async/await. In this take, we’ll show how advancements in ES2017 can make async code much better.

Think of these async features as improvements, not replacements. These new features build on top of the humble callback function. What you already know about JavaScript allows us to adopt these new features. In JavaScript, it’s seldom one aspect used versus another but a combination of the two.

To begin, we’ll build on top of this humble callback function:

const addByTwo = (x) => x + 2;


We’ll use ES6 arrow functions to make the code more concise. This puts more focus on async operations.

Callbacks

The humble callback function has some advantages because it is simple. Deferring execution with a timeout, for example, is done this way:

setTimeout((n) => console.log(addByTwo(n)), 1000, 2);


setTimeout takes a callback as a parameter and defers execution. This works well, but what happens when there are multiple callbacks? Callbacks can depend on the result of each one, which leads to the following:

setTimeout((p) =>
  setTimeout((l) =>
    setTimeout((n) =>
        console.log(addByTwo(n)),
      1000, addByTwo(l)),
    1000, addByTwo(p)),
  1000, 2);


This is what is often known as the pyramid of doom. Chained callback functions must be nested at several levels. This makes the code brittle and hard to understand. As a quick exercise, imagine how hard it is to add one more async operation to this.

To summarize this code, execution is deferred for three seconds, and the result is six.

Promises

Promises can make the above easier to work with. Start by abstracting the async operation into a Promise:

const fetchAddByTwoPromise = (p) => new Promise(
  resolve => setTimeout((n) => resolve(addByTwo(n)), 1000, p));


For this example, we only care about the resolve function, which executes the callback function: a parameter p sets which number gets added by two.

With a Promise in place, it is now possible to do this:

fetchAddByTwoPromise(2)
  .then((r) => fetchAddByTwoPromise(r))
  .then((r) => fetchAddByTwoPromise(r))
  .then((r) => console.log(r));


Note how clean this is and how maintainable it is. Code changes are cleaner because you no longer care where they sit in the pyramid. The then method can return a Promise if it wants to continue making async calls. In the end, the result goes into the console’s output.

The async journey does not end with Promises. ES2017 introduces async/await, which builds on top of this concept.

Async/Await

You need a function that returns a Promise to use async/await. This function must be prefixed with async before it can use await. For this example, create an async function that returns a Promise<number>:

const asyncAwaitExample = async (n) => {
};


Inside this async function, it can have the following:

let result = await fetchAddByTwoPromise(n);
result = await fetchAddByTwoPromise(result);
return await fetchAddByTwoPromise(result);


Note the code now reads more like synchronous code. Each awaits returns a fulfilled Promise, so it is building on top of the Promise abstraction. A let allows the variable to be mutable and is reused with each call. Adding more async operations is a simple matter of adding more lines of code.

To get the result, we can call the async function and check the returned Promise:

asyncAwaitExample(2).then((r) => console.log(r));


One way to see this is callbacks are the backbone of a Promise. And a Promise is now the backbone of async/await. This is the beauty of modern JavaScript. You are not relearning the language but building on top of existing expertise.

Pitfalls

The code samples above take around three seconds to complete. This is because a Promise suspends execution until fulfilled.

In async/await, the line of code doing the await suspends execution in the same manner. For this particular use case, the result is valuable because it is dependent on the overall result. This makes it so that the code cannot run in parallel because of this dependency.

In cases where there are no dependencies between async operations. There might be an opportunity to run everything in parallel. This speeds up execution since it’s not having to wait.

This is where both a Promise and async/await can work together:

const pitfallExample = async(n) => {
  return await Promise.all([
    fetchAddByTwoPromise(n),
    fetchAddByTwoPromise(n),
    fetchAddByTwoPromise(n)]);
};


Because each async operation fires at the same time, the overall runtime is down to one second. Combining both a Promise and async/await makes the code more readable. Keep this in mind when working with async code, no need to make customers wait longer than they should.

To fire up this async function, do:

pitfallExample(2).then((r) => console.log(r.reduce((x, y) => x + y)));


Note Promise.all returns an array of the results. Each async operation result that ran in parallel will be in the array. A reduced function can take it from there and add up a total.

Conclusion

Asynchronous operations in JavaScript have evolved.

The humble callback solves simple use cases, but as complexity grows it falls flat.

A Promise builds on top of callbacks via an object that wraps around a callback. This makes complex async code easier to think about.

To make the code readable, async/await builds on top of Promises to make it look like synchronous code. If the code can run in parallel, both a Promise and async/await can work together.

In JavaScript, there is no false dichotomy. Features build on top of each other to exploit current expertise. Mastering callbacks puts you on the path to master Promises and async/await.

Jscrambler

The leader in client-side Web security. With Jscrambler, JavaScript applications become self-defensive and capable of detecting and blocking client-side attacks like Magecart.

View All Articles

Must read next

Javascript

Asynchronous Operations in React-Redux

Setting up asynchronous operations in React can be quite a challenge. This tutorial explains how to use Redux-Thunk to do much of this work.

January 29, 2019 | By Camilo Reyes | 5 min read

Web Development

How To Use Redux Persist in React Native with Asyncstorage

The Redux Persist library provides an easy way to save a Redux store in the local storage of React Native apps. In this post, we explore how to set it up.

January 8, 2021 | By Aman Mittal | 15 min read

Section Divider