DEV Community

Cover image for Asynchronous JavaScript Under 5 Minutes
Phillip Shim for Atlassian

Posted on • Updated on

Asynchronous JavaScript Under 5 Minutes

 

JavaScript makes use of callbacks, promises, async and await features to support Asynchronous Programming. We won't dive into too many details with each topic, but this article should be a gentle introduction to get you started. Let's commence!

 

Example setup

Take a look at this simple example. We have an initial array with pre-filled numbers, 'getNumbers' function which loops over the array and outputs each item in the array and the 'addNumber' function to receive a number and add it to the array.

const numbers = [1, 2];

function getNumbers() {
  numbers.forEach(number => console.log(number))
}

function addNumber(number) {
  numbers.push(number);
}

getNumbers(numbers) // 1, 2
addNumber(3);
getNumbers(numbers) // 1, 2, 3

 

The problem

Now, let's suppose both of our function calls take some amount of time to execute because we are making requests to a backend server. Let's mimic it by using built-in setTimeout methods and wrap our logic inside them.

const numbers = [1, 2];

function getNumbers() {
  setTimeout(() => {
    numbers.forEach(number => console.log(number))
  }, 1000)
}

function addNumber(number) {
  setTimeout(() => {
  numbers.push(number)
  }, 2000)
}

getNumbers(numbers) // 1, 2
addNumber(3)
getNumbers(numbers) // 1, 2 ... Why?

Take a look at the console now. It's behaving differently than before. This is because the 'addNumber' function takes 2 seconds to run and the 'getNumbers' function takes a second to run. Therefore, 'addNumber' function gets executed after two of our 'getNumbers' get called. 'addNumber(3)' function call won't wait for its previous line to finish.

 

Callbacks

Invoking asynchronous calls line by line won't work in this case. Is there any other way to make sure a function gets executed only after another function finishes executing? Callbacks can help us! In javascript, functions can get passed around as arguments. Therefore, we could have 'getNumbers' function get passed into addNumber function and execute it once a number has been added.

const numbers = [1, 2];

function getNumbers() {
  setTimeout(() => {
    numbers.forEach(number => console.log(number))
  }, 1000)
}

function addNumber(number, callback) {
  setTimeout(() => {
  numbers.push(number)
  callback();
  }, 2000)
}

getNumbers(numbers) // 1, 2
addNumber(3, getNumbers) // 1, 2, 3

Here is the flow of our codebase. 'getNumbers' is invoked after 1 second. 'addNumbers' is invoked after 2 seconds (1 second after 'getNumbers'). After it pushes the number to the array, it calls 'getNumbers' again, which takes an additional 1 second. The program fully terminates after 3 seconds. To learn more about callbacks, I wrote an article in-depth before.

 

Promises

Here is the rewrite of the same code. We will no longer use a callback and call it directly so let's modify our 'addNumber' function to not take in the 2nd argument anymore. Instead, it will return a promise using new Promise() keyword immediately. A promise is able to use resolve and reject, given from arguments that you can call after certain actions. If everything goes well, you can call resolve().

 

const numbers = [1, 2];

function getNumbers() {
  setTimeout(() => {
    numbers.forEach(number => console.log(number))
  }, 1000)
}

function addNumber(number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      numbers.push(number);
      resolve();
    }, 2000)
  });
}

addNumber(3).then(getNumbers) // 1, 2, 3 after 3 seconds

When the promise is actually returned, we can chain it by using then keyword. You can then pass in a function definition to be called after your promise is resolved! Awesome! However, what if there was an error such as a network timeout? We could use the reject keyword and indicate that an action was unsuccessful. Let's manually reject it.

const numbers = [1, 2];

function getNumbers() {
  setTimeout(() => {
    numbers.forEach(number => console.log(number))
  }, 1000)
}

function addNumber(number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      numbers.push(number);
      const isAdded = false;
      if (isAdded) {
        resolve();
      } else {
        reject("There was an error")
      }
    }, 2000)
  });
}

addNumber(3).then(getNumbers).catch((e) => console.log(e)) // There was an error

Notice that we can pass in a string which is caught by using .catch and is available via its first argument. We could do the same thing with the resolve method as well by passing in some data and receive it inside the then() method.

 

Async & Await

Let's take the same code and use async and await! Here is a spoiler! We will still need to return a promise, but the way we handle it is different. Take a look.

const numbers = [1, 2];

function getNumbers() {
  setTimeout(() => {
    numbers.forEach(number => console.log(number))
  }, 1000)
}

function addNumber(number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      numbers.push(number);
      const isAdded = true;
      if (isAdded) {
        resolve();
      } else {
        reject("There was an error")
      }
    }, 2000)
  });
}

async function initialize() {
  await addNumber(3);
  getNumbers();
}

initialize(); // 1, 2, 3

Instead of chaining then and catch to the addNumber invocation, we created a function called initialize. Using the 'await' keyword requires its wrapper function to have 'async' keyword prepended. Also, the 'await' keyword makes our code more intuitive to reason about because our code reads line by line now even though it's async!

Now, How about error handling?

const numbers = [1, 2];

function getNumbers() {
  setTimeout(() => {
    numbers.forEach(number => console.log(number))
  }, 1000)
}

function addNumber(number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      numbers.push(number);
      const isAdded = false;
      if (isAdded) {
        resolve();
      } else {
        reject("There was an error")
      }
    }, 2000)
  });
}

async function initialize() {
  try {
    await addNumber(3);
    getNumbers();
  } catch (e) {
    console.log(e);
  }
}

initialize(); // There was an error

Let's use try and catch inside our initialize function. If a promise is rejected, our catch block will run.

 

Summary

We learned a few different ways on how to handle different methods of handling asynchronous JavaScript. As for me, I personally prefer writing async and await at all times of how easy it is to write and think about. But others have their places especially callbacks as some APIs only support them. Thank you for reading and let's write some serious code with our newly acquired knowledge!

This example code was inspired by Brad Traversy's youtube video.

 

Discussion (2)

Collapse
kalebealvs profile image
Kalebe Alves

Actually, you don't need to return a promise on the addNumber function. Not explicitly at least.
If you declare it as an async function, its return will automatically be wrapped into a promise resolution. If you throw an exception, it will be the rejection for said function.
To summarize, an async function will run as a microtask, working as a promise.
I think you already knew that, just don't wanna let that go unoticed.

Collapse
shimphillip profile image
Phillip Shim Author

Thanks for sharing this. It's a wonderful insight.