DEV Community

Cover image for What are Promises & Async/Await in JavaScript?
Afan Khan
Afan Khan

Posted on • Originally published at javascript.plainenglish.io

What are Promises & Async/Await in JavaScript?

When fetching data from another source on the Internet through an API, we cannot afford to halt the execution of the entire application while fetching data from that source. We must perform those operations simultaneously.

However, the design of JavaScript cannot achieve that. As I explained in this article, JS works synchronously. It can only execute one line of code sequentially from top to bottom. However, we require an asynchronous setting for JavaScript to run multiple lines or processes together in a sequence to avoid those halts.

Now, who can help facilitate that async functionality? Promises! They do not represent or conduct the asynchronous operation itself. Promises are an object that gets returned after the completion or failure of an asynchronous function with its output and resulting values.

What represents an async operation? Async/await keywords are the modern solution introduced in ES8 (2017). We can create asynchronous functions using these keywords, and they return a Promise when it completes execution or faces a problem. It is part of a special syntax.

Imagine async/await keywords as newer ways to create Promises after ES6. They help disguise the asynchronous code as synchronous code. The await keyword acts as an indicator for async to execute that particular Promise simultaneously and stop for it.

Asynchronous operations do not stop the JavaScript engine from continuing the execution of the remaining program. These operations are processed in the background while the remaining lines get executed.

JavaScript and precise Promises remain on hold for these values to get yielded later when the operation completes execution.

console.log(fetch("https://icanhazdadjoke.com/"));
Enter fullscreen mode Exit fullscreen mode

I’m trying to fetch data from an external API that provides dad jokes for free. And when I log the result of that fetch() method, it returns a Promise.

Promise Dad Joke Outcome in Console

Can you see the [[PromiseState]] property value? It is a part of the "States of Promises," and we will discuss them soon.

When we request data from an API without async/await, JavaScript executes the fetch() method immediately without waiting for the data to return.

It proceeds to the upcoming code line, where functions use the data from that external source. If the data fetching fails, how can those functions use the data?

JS immediately goes to the upcoming line, and the functions using the data from the API receive nothing in their hands. Those functions are forced to return undefined or null.

Usually, API requests take a few seconds to give the data to us. But JavaScript doesn't like to wait for this. And we must make JavaScript more patient.

JavaScript has an interpreter that executes each line sequentially compared to other language compilers that translate the entire code file together.

Therefore, we use async/await with Promises. When using async/await, the data fetching happens concurrently behind the scenes while JavaScript executes the rest of the code.

I launched a new free eBook!

The eBook is a 4-step framework to build projects and learn skills that enable you to attain practical experience in your field and become a developer whom recruiters and clients cannot say "No" during the discovery call.

You can get it from Gumroad - 
[https://afankhan.gumroad.com/l/build-to-solve-problems](https://afankhan.gumroad.com/l/build-to-solve-problems)
Enter fullscreen mode Exit fullscreen mode

Introduction to Promises

A Promise is like an agreement between two parties to ensure that each will deliver the desired outcome to the other person. There are different states of these promises. Some people break theirs while others fulfil them. Anyone can make promises.

JavaScript Promises creates an agreement with us to deliver the output, whether successful or failure, from an asynchronous operation. They ensure that the async function will get executed in the background and the data structured in an object.

The Promise object generated by an asynchronous operation will produce a single value anytime in the future when the execution completes in the background, like a promise between humans. The single value could be the data after the fetching succeeds or the failure message.

Since Promises are objects, we can bind methods and callback functions to them to perform different operations from the data received by external sources. Before Promises, we used callback functions, but the syntax and efficiency have improved.

Instead of using and passing callback functions as arguments, Promises bind and attach callback functions to reduce the pyramid and callback hell.

Lastly, they are achieving the same task, but callbacks create hell for developers, and the efficiency takes a hit. Look at this callback hell.

Callback Hell reference from Amazing Enyichi Agu

Let’s handle a Promise now. More often than not, you will get Promises and respond to Promises rather than creating custom ones, and I will explain that in a moment. For our example, I will use an API providing random quotes from Luke Peavey on GitHub.

"use strict";

const randomQuote = async function () {
  try {
    await fetch("https://api.quotable.io/random")
      .then((response) => {
        return response.json();
      })
      .then((data) => {
        console.log(`${data.author}: ${data.content}`);
      });
  } catch (e) {
    console.log(`Error: ${e}`);
  }
};

randomQuote();
Enter fullscreen mode Exit fullscreen mode

JavaScript will log a random quote to my console whenever I call the randomQuote() method. Take a look at the output.

Quote 1

It is not returning undefined because I am using async/await. As I refresh the page, I get new random quotes from Luke's massive database.

Quote 2

Quote 3

Let me bring you back to my article before you get driven away by these quotes. Allow me to explain what I did in that small code.

First, you must understand that async and await are couples that cannot detach from each other. You cannot use an await without the async keyword and vice versa. Why do we use them? To facilitate operations that require extensive time for their execution.

I created an asynchronous function using the async keyword while defining it. I used the await keyword before calling a time-consuming function to indicate that this will take a few seconds to perform something, so the interpreter should continue while another thread fetches the data.

For us, that time-consuming function is called fetch(). It will take time to get the data from an external database. As fetch() returns a Promise, I attached callback functions. The then() method allows us to bind the callback functions to functions that return Promises. We indirectly attached them to the Promise.

The first then() method attached directly to the fetch() method will extract the object from the Promise returned by the fetch() method and convert it into JSON format. After that, I attached another then() to get the previous Promise and specifically access the object that contains the requested data without any other miscellaneous properties.

Output Image - afankhan.com

The above object represents the actual data fetched from the API. Inside the last callback function, I use the properties from this data object and log a sentence to the console with the quote content and author name.

You can manipulate the data received by the API in the last callback function or save it somewhere and use it later. We usually do the latter. Developers prefer to learn data fetching in Axios or any other library. I recommend getting your hands dirty with the fundamentals of JavaScript first with the Fetch API and then studying those libraries.

State of Promises

State of Promises diagram by Afan Khan - afankhan.com

A Promise belongs to either one of three states. The initial one is the “Pending” state. While an asynchronous function fetches the data from an external API, the Promise, which is the output of the fetch(), stays in the
“Pending” state.

Once the data transfer through an API is complete, the fetch() returns a Promise containing the data from the API. At this stage, it gets the “Resolved” statement. We will dive deeper into the “Resolve” and “Reject” states soon.

The .then() handles the fulfilled state when data gets fetched, like in the previous example. Furthermore, the .catch() method manages the rejected state. When you get a response, the Promise enters the Settle state, which either has been resolved or rejected.

Once a Promise gets settled in a resolved or rejected state, it cannot revert to a pending state either. It will have a single value once settled. Till then, the Promise remains pending, and developers wait for it to get a value while other code lines execute.

Lastly, when the fetch() method fails to get the data from an API, the Promise enters the “Rejected” state wherein we handle errors and failure messages to indicate the origin of the mistake for developers to fix it.

Synchronous Behavior

As I explained earlier, we cannot halt the execution of the remaining JS code for an API to send data to our code. This is what would happen if we didn’t use async/await.

"use strict";

// Function Definition

const todosEx = function () {
  fetch("https://jsonplaceholder.typicode.com/todos/2")
    .then((res) => {
      return res.json();
    })
    .then((data) => console.log(data));
};

// Real Testing

const test = () => {
  console.log(1);
    // After 1
  todosEx();
    // Before 2
  console.log(2);
};

test();
Enter fullscreen mode Exit fullscreen mode

We created two non-asynchronous functions. The todosEx() function fetches data from a dummy API provided by fellow developers, and the test() function calls different methods in a sequence.

Technically, JS should log 1 to the console first when test() gets invoked, and then the data from the API, followed by logging 2 to the console. However, the results are far from this theory.

Synchronous Output Image

It is because JavaScript uses an interpreter and doesn’t like to wait. Therefore, it pushes the API request to the Event Loop with the Call Stack and executes it whenever the rest of the lines get executed. If we use async/await, the results come closer to our hypothesis.

"use strict";

// Function Definition

const todosEx = async function () {
  await fetch("https://jsonplaceholder.typicode.com/todos/2")
    .then((res) => res.json())
    .then((data) => console.log(data));
};

// Real Testing

const test = async () => {
  console.log(1);
  await todosEx();
  console.log(2);
};

test();
Enter fullscreen mode Exit fullscreen mode

I inserted async/await pairs to both functions. The todosEx() required async/await because it invoked the time-consuming fetch() method, which returns a Promise, and the test() function got async/await because it is calling a function that returns a Promise.

Only the syntax looks synchronous, but async/await makes this asynchronous. I am logging the data to the console, which is a side effect. I am not directly returning the data. And that is why even test() needed async/await.

Since I am not returning anything else using the return keyword explicitly from the todosEx() function, it will give a Promise. Even if I try to store the data in another variable, it will return a Promise.

const todosEx = async function () {
  let dataSet;

  await fetch("https://jsonplaceholder.typicode.com/todos/2")
    .then((res) => {
      return res.json();
    })
    .then((data) => {
      dataSet = data;
    });

  return dataSet;
};

console.log(todosEx()); // Expected Output: Promise()
Enter fullscreen mode Exit fullscreen mode

JavaScript occasionally becomes a mystery. And if we remove async/await from the above code snippet, we will receive undefined because JavaScript didn’t wait for the data to get fetched and immediately jumped to the console.log() line.

Hence, whenever there is a Promise, there will be async/await in each function.

Creating Promises

Other than reacting to methods that return a Promise, we can also create them manually. We usually do this for legacy asynchronous tasks, and avoid using them frequently.

The API provides a Promise constructor to create instances of it. Till now, the fetch() method returned a Promise and we consumed it. Now, let us create a custom function that returns a Promise.

"use strict";

let flag = false;

const customPromise = function () {
  return new Promise((resolve, reject) => {
    if (flag) {
      resolve("Hey, this is resolved");
    } else {
      reject("Hey, your request got rejected");
    }
  });
};

const ourFunction = async function () {
  flag = true;
  console.log(await customPromise());
};

ourFunction();
Enter fullscreen mode Exit fullscreen mode

First, I created a variable called flag. If true, the Promise will resolve the request successfully and return a string. Otherwise, it will reject. Furthermore, I created a function called customPromise() that returns a Promise object with two parameters. I will discuss them shortly.

TheourFunction() is an async/await-based function because it calls the customPromise() method returning a Promise. We used the async/await with fetch() method because even that returned a Promise and took a few seconds to resolve the request.

I also toggled the flag to true for the Promise to resolve my request, and since we used async, I used the await keyword to call the Promise method.

Lastly, I invoked ourFunction(). Since the flag stated true, the async/await used it appropriately and got the following result.

Custom Promise Output

If I remove the flag = true, I will receive an error. See, when a Promise gets resolved, it gets treated as an appropriate response. Therefore, we do not receive something in the console automatically. We can use the object if we wish or ignore it. But we cannot eliminate a rejection, and JavaScript posts an error.

For the same reason, I could remove the console.log() while removing flag = true.

const ourFunction = async function () {
  // flag = true;
  await customPromise();
};

ourFunction();
Enter fullscreen mode Exit fullscreen mode

Rejected State

I can also save the value returned by the resolve function to another variable and then manipulate it for precautionary reasons.

const ourFunction = async function () {
  flag = true;
  const result = await customPromise();

  console.log(result);
};

ourFunction();
Enter fullscreen mode Exit fullscreen mode

I can use the data inside the result variable and manipulate that indirectly. Now, let us understand resolve() and reject().

Resolve and Reject

The callback function of the Promise constructor takes two arguments in the form of two extra callback functions called Executors. They are not values but functions that can get invoked. A Promise can either resolve or reject a request. We set custom responses when creating a custom Promise.

The fetch() method created these responses for us earlier, but now we must make them manually. When the API successfully returned the data, the fetch() method resolved the request and returned it as an object. Let us replicate the same functionality using the previous example.

"use strict";

let flag = false;

// Imagine this as the fetch() method code.
const customPromise = function () {
  // Even fetch() returns a Promise like this with the Response object.
  return new Promise((resolve, reject) => {
    if (flag) {
      resolve("Hey, this is resolved");
    } else {
      reject("Hey, your request got rejected");
    }
  });
};

// Imagine this as our function that calls the fetch() method
const ourFunction = async function () {
  flag = true;
  return await customPromise();
};

console.log(ourFunction()); // Expected Output: Promise()
Enter fullscreen mode Exit fullscreen mode

Consider the customPromise() method as the pre-defined code for the fetch() method and ourFunction() that invokes the fetch() method. We get the same output from the custom environment. We return a Promise and resolve it with a Response, like the pre-defined fetch() method.

Makes sense? Those functions return a Promise, as with the fetch() method. The fetch() method does nothing but return a Promise, as I did with customPromise().

We can also return an object through the resolve() function.

"use strict";

let flag = false;

const customPromise = function () {
  return new Promise((resolve, reject) => {
    if (flag) {
      // resolve("Hey, this is resolved");
      resolve({
        name: "Afan Khan",
        publication: "JavaScript in Plain English",
        article: "What is a Promise and Async/Await",
      });
    } else {
      reject("Hey, your request got rejected");
    }
  });
};

const ourFunction = async function () {
  flag = true;
  console.log(await customPromise());
};

ourFunction();
Enter fullscreen mode Exit fullscreen mode

Object through resolve() Output

const ourFunction = async function () {
  flag = true;
  await customPromise().then((data) =>
    console.log(`Name: ${data.name} & Article: ${data.article}`)
  );
};

ourFunction();
Enter fullscreen mode Exit fullscreen mode

I extended the previous code snippet and directly called the .then() method as it returns a Promise.

Extending the previous example

Usually, we use the try-and-catch block inside the definition of the returned Promise from customPromise().

"use strict";

const customPromise = function () {
  return new Promise((resolve, reject) => {
    try {
      setTimeout(() => {
        resolve({
          name: "Afan Khan",
          publication: "JavaScript in Plain English",
          article: "What is a Promise and Async/Await",
        });
      }, 1500);
    } catch (e) {
      reject("Hey, your request got rejected");
    }
  });
};

const ourFunction = async function () {
  await customPromise().then((data) =>
    console.log(`Name: ${data.name} & Article: ${data.article}`)
  );
};

ourFunction();
Enter fullscreen mode Exit fullscreen mode

Inside the try block, I added an asynchronous setTimeout() method to demonstrate the use of a Promise resolving a request that takes 1.5 seconds for async/await while the rest of the code gets executed. In this scenario, we are purposely pausing the pausing the execution of the Promise.

Code Output GIF

We can also use catch, finally, and other methods with
functions that return Promises, like the fetch() method.

Promises without functions

You’re right. We can also directly Promises without a function. You can create an instance of the Promises constructor, store it in a variable, and precisely call those methods. But usually, we deal with Promises returned by other functions.

"use strict";

const promiseExample = new Promise((resolve, reject) => {
  try {
    resolve({
      status: "Test Confirmed",
      name: "Afan Khan",
    });
  } catch (e) {
    reject("Test Rejected");
  }
});

promiseExample.then((data) => console.log(data.status)); // "Text Confirmed"
Enter fullscreen mode Exit fullscreen mode

Instead of writing the try-catch block inside the Promise callback function, you can bind it outside, but I don’t prefer using that.

const promiseExample = new Promise((resolve, reject) => {
  reject("Test Rejected");
});

promiseExample.catch((error) => console.log(error)); // "Text Rejected"
Enter fullscreen mode Exit fullscreen mode

Promise Chain

If you noticed from the first code snippet of this article, I had a .then() chain. It is because the first .then() method collected JSON from the Promise object and returned another Promise, and the second chain used that converted JSON to manipulate the data.

The .then() always returns another new Promise. We use the data provided by the previous Promise to create another Promise, convert it into JSON, and so on. It is a chain that keeps on going.

"use strict";

const promiseExample = new Promise((resolve, reject) => {
  try {
    resolve({
      name: "Afan Khan",
      age: "17",
    });
  } catch (e) {
    reject("Test Rejected");
  }
});

promiseExample
  .then((data) => {
    return data;
  })
  .then(({ name, age }) => {
    console.log(name, age);
  });
Enter fullscreen mode Exit fullscreen mode

We can chain then() methods together as each returns a new Promise instead of creating a callback hell or a pyramid of doom that existed before ES6 for asynchronous operations. Before ES6, developers used callback functions repeatedly to perform data fetching.

You can add the catch() method after a chain of Promises with .then() methods to handle errors.

Additionally, we can eliminate the requirement of the .catch() method altogether and handle the rejected state through then() itself. It accepts two parameters, the first for the resolved state and the other for the rejected state.

This only happens when you manipulate the data from different callback functions and not within the .then() method. There are numerous ways to use the same method.

"use secret";

const secretVal = 5;

const thenEx = new Promise((resolve, reject) => {
  if (secretVal === 5) {
    resolve("Resolved");
  } else {
    reject("Rejected");
  }
});

const success = (value) => {
  console.log(value);
};

const failure = (value) => {
  console.error(value);
};

thenEx.then(success, failure);
Enter fullscreen mode Exit fullscreen mode

More often than not, you will not create custom Promises. We usually consume and deal with Promises when working with APIs, Databases, etc.

We chain then() methods and consume the data to handle Promises. Furthermore, we use methods like Promise.all() to pass multiple Promises in an array and get values from them collectively if they all get resolved.

If a Promise gets resolved, but the developer calls .then() repeatedly, the callback function will get triggered again, and another Promise will get generated. But if the Promise gets rejected, the callback function also stops.

Since each .then() returns a Promise, we can manually make a cluster of multiple .fetch() statements, print the incoming data through the log() method and make a loop. Take a look at this.

"use strict"

const chainExample = async function () {
  await fetch("https://jsonplaceholder.typicode.com/todos/5")
    .then((res) => {
      return res.json();
    })
    .then(async (data) => {
      console.log(data);

      return fetch("https://jsonplaceholder.typicode.com/todos/6");
    })
    .then((res) => {
      return res.json();
    })
    .then((data) => {
      console.log(data);

      return fetch("https://jsonplaceholder.typicode.com/todos/7");
    })
    .then((res) => {
      return res.json();
    })
    .then((data) => {
      console.log(data);
    });
};

chainExample();
Enter fullscreen mode Exit fullscreen mode

This example is not ideal at all in the real world. Instead, we have the Promise.all() methods and more. Something like the above code snippet looks like a callback hell and should be discouraged.

Summary

A Promise is an object value returned by an asynchronous method like fetch() that requires async/await for data fetching and rendering. It resolves or rejects a request. We use async/await with functions that produce Promises.

We use Promises when we know that specific tasks may take time to execute and shift those tasks to the background for the rest of the program execution. Everything else doesn't need to wait for the time-consuming tasks to get completed.

You can bind .then() with the fetch() method because it returns a Promise. We can only use .catch().finally(), and similar ones on a Promise. Instead of using these methods, developers prefer using a try-catch block inside an async/await function with a Promise to handle errors.

Promises were introduced in ES6, and later async/await appeared in ES8. The async/await keywords improved Promises in terms of syntax and efficiency. We use these keywords to fetch data from other external sources or databases while executing the rest of our code.

It allows us to make JavaScript more asynchronous. The word "simultaneously" represents the meaning of asynchronous in the JavaScript and Computer Science lingo.

We cannot use async without await and vice-versa. The await keyword is used to wait for a Promise object with data from an API or DB. While we wait, other things get executed. When we use async/await, JS allows us to use the try-catch block. It is a good practice.

When someone uses async, you must automatically understand that a Promise is coming up next.


If you want to contribute, comment with your opinion and if I should change anything. I am also available via E-mail at hello@afankhan.com. Otherwise, Twitter (X) is the easiest way to reach out - @justmrkhan.

The notion document containing the resources/sources used to write this piece with drafts and an extra section — https://crackjs.com/promises-article.

Top comments (0)