There’s a special syntax you can use in JavaScript that makes working with promises easier. It's called “async/await", and it’s surprisingly straightforward to understand and use.

In this article, we'll discuss:

  1. What are asynchronus functions?
  2. How promises work in JavaScript
  3. Async/Await basics
  4. How to use async/await with error handling
  5. How an async function returns a promise
  6. How to use promise.all()

So let's dive in.

What are asynchronus functions?

The term asynchronus refers to a situation where two or more events don't happen at the same time. Or in simple terms, multiple related things can happen without waiting for the previous action to complete.

In JavaScript, asynchronus functions are really important because of the single threaded nature of JavaScript. With the help of asynchrnous functions, JavaScript's event loop can take care of other things when the function is requesting some other resource.

You'd use aysnchronous code, for example, in API's that fetch a file from the network, when you're accessing a database and returning data from it, when you're accessing a video stream from a web cam, or if you're broadcasting the display to a VR headset.

How Promises Work in JavaScript

The Promise object in JavaScript represents an asynchronous operation (and its resulting value) that will eventually complete (or fail).

A Promise can be in one of these states:

  • pending: initial state, neither fulfilled nor rejected.
  • fulfilled: meaning that the operation was completed successfully.
  • rejected: meaning that the operation failed.

Promise-in-Javascript

The function passed to a new promise is called the executor. It's arguments (resolve and reject) are called callbacks that JavaScript itself provides. When the executor gets the result, whether it's now or later it doesn’t matter – it should call one of these callbacks.

Here's an example of a Promise:

const myPromise = new Promise(function(resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 300);
});

And here are examples of a fulfilled vs a rejected promise:

// fulfilled promise
let  promise = new  Promise(function(resolve, reject) {
 setTimeout(()  => resolve(new Error("done!")), 1000);
});

// resolve runs the first function in .then
promise.then(
  result => alert(result), // shows "done!" after 1 second
  error => alert(error) // doesn't run
);
// rejected promise
let promise =  new  Promise(function(resolve, reject)  { 
 setTimeout(()  => reject(new Error("Whoops!")), 1000);
});

// reject runs the second function in .then
promise.then(
  result => alert(result), // doesn't run
  error => alert(error) // shows "Error: Whoops!" after 1 second
);

Async/Await Basics in JavaScript

There are two parts to using async/await in your code.

First of all, we have the async keyword, which you put in front of a function declaration to turn it into an async function.

An async function is a function that knows to expect the possibility that you'll use the await keyword to invoke asynchronous code.

The async keyword is added to functions to tell them to return a promise rather than directly returning the value.

const loadData = async () => {
  const url = "https://jsonplaceholder.typicode.com/todos/1";
  const res = await fetch(url);
  const data = await res.json();
  console.log(data);
};
loadData();
// Console output
{
  completed: false,
  id: 1,
  title: "delectus aut autem",
  userId: 1
}

How to Use Async/Await with Error Handling

We can handle errors using a try catch block like this:

const loadData = async () => {
  try{
	  const url = "https://jsonplaceholder.typicode.com/todos/1";
	  const res = await fetch(url);
	  const data = await res.json();
	  console.log(data);
  } catch(err) {
    console.log(err)
  }
};

loadData();

The above try-catch will only handle errors while fetching data such as the wrong syntax, wrong domain names, network errors, and so on.

When you want to handle an error message from the API response code, you can use res.ok (res is the varaiable to which response is stored). It will give you a Boolean with the value true if the response code is between 200 and 209.

const loadData = async () => {
  try{
	  const url = "https://jsonplaceholder.typicode.com/todos/qwe1";
	  const res = await fetch(url);
	  if(res.ok){ 
	    const data = await res.json();
	    console.log(data);
	  } else {
	    console.log(res.status); // 404
	  }
  } catch(err) {
    console.log(err)
  }
};

loadData();

// OUTPUT
// 404

How an Async Function Returns a Promise

This is one of the traits of async functions — their return values are guaranteed to be converted to promises. To handle data returned from an async function we can use a then keyword to get the data.

const loadData = async () => {
  try{
    const url = "https://jsonplaceholder.typicode.com/todos/1";
    const res = await fetch(url);
    const data = await res.json();
    return data
  } catch(err) {
    console.log(err)
  }
};

const data = loadData().then(data => console.log(data));

💡 PRO TIP :
if you want to use an async-await to handle the returned data you can use an IIFE, but it is only available in Node 14.8 or higher.

// use an async IIFE
(async () => {
  const data = await loadData();
  console.log(data);
})();

await only works inside async functions within regular JavaScript code. But you can use it on its own with JavaScript modules.

How to Use Promise.all() in JavaScript

Promise.all() comes in handy when we want to call multiple API's.

Using a traditional await method, we have to wait for each request to complete before going on to the next request. This becomes a problem when each request takes some time to complete. This can easily add up and slow things down.

Using Promise.all(), we can call each of these API's in parallel. (This is a bit of an oversimplification – for more details checkout this amazing article).

Please be carefull when using Promise.all, though – if one of the await requests fails, it will fail altogether.

const loadData = async () => {
  try{
    const url1 = "https://jsonplaceholder.typicode.com/todos/1";
    const url2 = "https://jsonplaceholder.typicode.com/todos/2";
    const url3 = "https://jsonplaceholder.typicode.com/todos/3";
    const results = await Promise.all([
      fetch(url1),
      fetch(url2),
      fetch(url3)
    ]);
    const dataPromises = await results.map(result => result.json());
    const finalData = Promise.all(dataPromises);
    return finalData
  } catch(err) {
    console.log(err)
  }
};

const data = loadData().then(data => console.log(data));
// Console output
[{
  completed: false,
  id: 1,
  title: "delectus aut autem",
  userId: 1
}, {
  completed: false,
  id: 2,
  title: "quis ut nam facilis et officia qui",
  userId: 1
}, {
  completed: false,
  id: 3,
  title: "fugiat veniam minus",
  userId: 1
}]

Conclusion

In most situations we can use async/await with a try catch block to handle both results and errors.

At the moment, await won’t work in top-level code. This means that when we’re outside any async function, we’re syntactically unable to use await. In this case, it’s regular practice to add .then/catch to handle the final result or falling-through error.

Top level await is available from Node.js v14.8 onwards and in ES modules only. Read more here: Top-level await is available in Node.js modules | Stefan Judis Web Development

Resources that helped me

Before We End...

👋 Do you want to code and learn along with me? You can find the same content here in my blog. Just open up your favorite code editor and get started.

Milind Soorya
I’m Milind. I’m a full-stack developer and blogger. On this site we explore the strategies and tools that help us to code better and cleaner.

Let's connect. You will find me active on Twitter (@milindsoorya). Please feel free to give a follow.