Skip to main content

Asynchronous JavaScript Programming and Callbacks

Asynchronous (async) means "not synchronous".

An event is asynchronous if it does not wait for another event to complete its processing before starting its own execution.

In other words, an asynchronous function is a function that runs in its own timing—without waiting for another function to finish its execution first.

The setTimeout() method is a typical example of an async function because it invokes its callback argument in a given time—irrespective of the ongoing execution of other functions.

But what exactly is the difference between an asynchronous event and a synchronous one? Let's find out.

Asynchronous vs. Synchronous Events – What's the Difference?

An asynchronous event is a program that can happen at any convenient time.

A synchronous event is an activity that must occur at a specific point in time.

In other words, an asynchronous event permits functions to happen independently—without waiting in line for the completion of other programs.

However, a synchronous event requires functions to happen one after the other. So, function B must wait in line for function A to complete its execution before commencing its own.

Synchronous programming most often affects an app's usability. Because users usually have to stare at the spinning cursor—waiting for a function to finish its processing before the program can become usable again.

info

Blocking is a term used to refer to the spinning cursor moments (when the browser appears frozen). In such a period, the currently running operation blocks the browser from doing other things until it has finished its execution.

On the other hand, asynchronous programming allows users to do other things while waiting for another function to finish its processing.

But how exactly does JavaScript implement asynchronous programming? Let's find out below.

How to Implement Asynchronous Programming in JavaScript

JavaScript, by default, is a single-threaded programming language. However, modern browsers allow us to implement asynchronous programming in the following two ways:

  1. Passing callbacks into functions.
  2. Attaching callbacks to promise objects.

But what exactly is a callback, and what does a promise mean? Let's find out.

What Is a Callback?

A callback is a function A passed as an argument into a function B and invoked inside function B.

In other words, after passing function A into function B, we expect function B to callback (invoke) function A at a given time.

note
  • Callback functions are sometimes called call-after functions.
  • The specific time function B will callback function A may be immediately or later in the future.
  • If the callback's execution happens immediately, the call-after is called a synchronous callback.
  • If the callback's invocation occurs at some later time, the call-after is called an asynchronous callback.

Let's see some examples of a synchronous and asynchronous callback function.

Example 1: Synchronous callback function

In the snippet below, bestColor() is a callback function because:

  1. We passed it as an argument to the showBestColor() function
  2. We invoked it inside showBestColor()
function bestColor(color) {
console.log("My best color is " + color + ".");
}

function showBestColor(callback, choice) {
callback(choice);
}

showBestColor(bestColor, "White");

// The invocation above will return:
"My best color is White."

Try it on StackBlitz

The bestColor() callback function above is a synchronous callback because we programmed its execution to happen immediately.

Let's now see an example of an asynchronous callback function.

Example 2: Asynchronous callback function

In the snippet below, bestColor() is a callback function because:

  1. We passed it as an argument to the setTimeout() function
  2. The bestColor() function got invoked inside setTimeout()
function bestColor(color) {
console.log("My best color is " + color + ".");
}

setTimeout(bestColor, 3000, "White");

// The invocation above will return:
"My best color is White."

Try it on StackBlitz

The bestColor() callback is an asynchronous callback because we programmed its execution to happen at a given time—after 3000 milliseconds (3 seconds).

Keep in mind that an alternate way to write the code above is like so:

setTimeout(
function (color) {
console.log("My best color is " + color + ".");
},
3000,
"White"
);

// The invocation above will return:
"My best color is White."

Try it on StackBlitz

In the snippet above, we passed a complete function—not a reference—as a callback argument to the setTimeout() method.

Now, beware that we implemented the "inversion of control (IoC)" programming principle in the asynchronous callback snippets above.

But what exactly is "inversion of control," I hear you ask? Let's find out below.

What Is Inversion of Control?

Inversion of control (IoC) occurs when you transfer the control of your code's execution to a third party.

The IoC programming principle commonly happens while writing a callback program—where you provide your function as an argument to a third-party's function.

For instance, consider the code below:

function bestColor(color) {
console.log("My best color is " + color + ".");
}

setTimeout(bestColor, 3000, "White");

Try it on StackBlitz

In the snippet above, setTimeout() is a third-party code that belongs to the browser's Window object.

On the other hand, bestColor() is our own code (a code we wrote ourselves).

Now, since we passed our code (bestColor()) as an argument to a third party's code (setTimeout()). It implies that we've transferred the control of our function's execution to setTimeout().

In other words, we inverted the control of bestColor()'s execution from ourselves to the third party.

Therefore, setTimeout() has full control to determine how bestColor() will get executed.

note

Inversion of control could be problematic if you do not design your code with logic to mitigate trust issues.

So, now that we know what callbacks are, we can talk about promises.

What Is a Promise?

A JavaScript promise refers to the object you create from JavaScript's built-in constructor function—called Promise.

Here's an example:

new Promise(function () { });

// The invocation above will return:
Promise { <state>: "pending" }

Try it on CodeSandbox

In the snippet above, we created a promise object by passing a callback function into JavaScript's built-in Promise constructor function.

Keep in mind that a callback function is a required argument of the Promise constructor function. If omitted, JavaScript will throw an Uncaught TypeError.

Here's an example:

new Promise();

// The invocation above will return:
"Uncaught TypeError: undefined is not a function"

Try it on StackBlitz

Promise's state

A promise is always in one of three states: pending, fulfilled, or rejected.

When you have a pending promise, it means you have an unfulfilled or unrejected promise.

In other words, a pending promise means you have not received any response concerning a specific promise.

Here's an example:

const myMomsPromise = new Promise(function () { });

console.log(myMomsPromise);

// The invocation above will return:
Promise { <state>: "pending" }

Try it on CodeSandbox

Suppose a promise returns a valid value. In that case, the promise's state will switch from its pending state to a fulfilled state.

Here's an example:

const myMomsPromise = new Promise(function (resolve, reject) {
resolve();
});

console.log(myMomsPromise);

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: undefined }

Now, suppose a promise could not return a valid value. In that case, the promise's state will change from its pending state to a rejected state.

Here's an example:

const myMomsPromise = new Promise(function (resolve, reject) {
reject();
});

console.log(myMomsPromise);

// The invocation above will return:
Promise { <state>: "rejected", <reason>: undefined }

Important stuff to know about promises

Keep these three essential pieces of info in mind whenever you choose to use promises.

  1. A promise is settled if it is either fulfilled or rejected.

  2. A settled promise is sometimes called a resolved (or locked in) promise.

  3. resolve—not fulfill—is the recommended name for the first parameter of a Promise constructor's callback argument. Developers recommend "resolve" because the first parameter does not always set a value to a fulfilled promise. In other words, the first parameter can resolve a promise object to a rejected state.

    Here's an example:

    const myMomsPromise = new Promise(function (resolve, reject) {
    resolve(Promise.reject());
    });

    console.log(myMomsPromise);

    // The invocation above will return:
    Promise { <state>: "rejected", <reason>: undefined }

So, now that we know what promises and callbacks are, let's continue our discussion on the two ways to implement asynchronous programming in JavaScript.

Remember, we said the two typical ways to implement asynchronous programming in JavaScript are:

  1. By passing callbacks into functions, and
  2. By attaching callbacks to a Promise object.

Let's see how the two techniques work.

How to Implement Async Programming by Passing Callbacks into Functions

Suppose your mom promised you some exceptional gifts if you pass your exam.

In such a case, you can represent mom's promise and its eventual settlement (fulfillment or rejection) by passing callbacks into functions like so:

// Create a variable:
const passedExam = true;

// Create a function that return mom's first promise:
function emitFirstProm() {
return "I get a book";
}

// Create a function that add mom's first promise to her second one:
function emitSecondProm(firstPromise, addFirstPromToSec) {
addFirstPromToSec(firstPromise() + ", a phone", passedExam);
}

// Create a function that add mom's previous promises to her third one:
function emitThirdProm(pastProms, addPastPromsToThird) {
addPastPromsToThird(pastProms + ", a laptop", passedExam);
}

// Create a function that add mom's previous promises to her fourth one:
function emitFourthProm(pastProms, addPastPromsToFourth) {
addPastPromsToFourth(pastProms + ", and a Jeep 🤩🎉✨", passedExam);
}

// Invoke each of the functions above asynchronously by using them as callback arguments:
emitSecondProm(emitFirstProm, function (pastPromPlusSec, IPassedMyExam) {
let finalResponse = null;
if (IPassedMyExam) {
emitThirdProm(
pastPromPlusSec,
function (pastPromsPlusThird, IPassedMyExam) {
if (IPassedMyExam) {
emitFourthProm(
pastPromsPlusThird,
function (pastPromsPlusForth, IPassedMyExam) {
if (IPassedMyExam) {
finalResponse = `Mom fulfilled her promise! ${pastPromsPlusForth}`;
}
}
);
}
}
);
} else {
console.error(
"Oops! Caught the following error: You did not pass your exam, so mom rejected her promise."
);
}
console.log(finalResponse);
});

// The invocation above will return:
"Mom fulfilled her promise! I get a book, a phone, a laptop, and a Jeep 🤩🎉✨"

Try it on StackBlitz

Here's what we did in the snippet above:

  1. We created a variable (passedExam) that confirms if you passed your exam (true).
  2. We created a function (emitFirstProm) that—when invoked—will return Mom's first promise.
  3. Thirdly, we created three other functions that—when invoked—will add mom's previous promise(s) to her latest one.
  4. Finally, we invoked each of the three functions asynchronously depending on whether passedExam's value is true or false.

Therefore, since passedExam's value is always true in our example above, all mom's promises will be fulfilled. As such, our callbacks will return "Mom fulfilled her promise! I get a book, a phone, a laptop, and a Jeep 🤩🎉✨".

Passing callbacks into functions—as we did above—was the default way to accomplish several asynchronous operations, which often leads to creating the "callback pyramid of doom."

However, the modern invention of promises provides a neater way to implement asynchronous programming. Let's see how.

How to Implement Async Programming by Attaching Callbacks to a Promise Object

Suppose your mom promised you some exceptional gifts if you pass your exam.

In such a case, we can represent mom's promise and its eventual settlement (fulfillment or rejection) by attaching callbacks to a promise object like so:

// Create two variables:
let finalResponse = null;
const passedExam = true;

// Create a function that return mom's first promise:
function emitFirstProm() {
return "I get a book";
}

// Create a function that add mom's first promise to her second one:
function emitSecondProm(firstPromise) {
return firstPromise + ", a phone";
}

// Create a function that add mom's previous promises to her third one:
function emitThirdProm(pastProms) {
return pastProms + ", a laptop";
}

// Create a function that add mom's previous promises to her fourth one:
function emitFourthProm(pastProms) {
return pastProms + ", and a Jeep 🤩🎉✨";
}

// Create mom's promise:
const myMomsPromise = new Promise(function (resolve, reject) {
if (passedExam) {
resolve(emitFirstProm());
} else {
reject("You did not pass your exam, so mom rejected her promise.");
}
});

// Invoke each of the functions above asynchronously by attaching them to myMomsPromise object:
myMomsPromise
.then(emitSecondProm)
.then(emitThirdProm)
.then(emitFourthProm)
.then(function (value) {
finalResponse = `Mom fulfilled her promise! ${value}`;
})
.catch(function (e) {
console.error(`Oops! Caught the following error: ${e}`);
})
.finally(function () {
console.log(finalResponse);
});

// The invocation above will return:
"Mom fulfilled her promise! I get a book, a phone, a laptop, and a Jeep 🤩🎉✨"

Try it on StackBlitz

Here's what we did in the snippet above:

  1. We created a variable (passedExam) that confirms if you passed your exam (true).
  2. We created a function (emitFirstProm) that—when invoked—will return Mom's first promise.
  3. Thirdly, we created three other functions that—when invoked—will add mom's previous promise(s) to her latest one.
  4. Fourthly, we created a promise object.
  5. Finally, we used the promise object's built-in methods—then() and catch()—to attach additional callback functions to the promise object. Then, depending on whether passedExam's value is true or false, the computer will invoke each attached function asynchronously.

Therefore, since passedExam's value is always true in the example above, all mom's promises will be fulfilled. As such, our callbacks will return "Mom fulfilled her promise! I get a book, a phone, a laptop, and a Jeep 🤩🎉✨".

14 Important Stuffs to Know about JavaScript's then(), catch(), and finally() Methods

Keep these essential pieces of info in mind whenever you choose to use JavaScript's then(), catch(), and finally() methods.

  1. Under the hood, each then() method returns a new promise object that is different from the original promise we assigned to the myMomsPromise variable.
  2. A promise object's then() method gets invoked when the promise chained to it is settled (fulfilled or rejected).
  3. A promise's then() method will not get invoked if the promise chained to it is pending.
  4. A then() method's callback argument won't run immediately. It gets invoked in the future—when the then() method receives a settled response.
  5. In a then chain, each then() method gets the returned value of the previous then() method.
  6. A then() method's callback argument needs to return a value. Otherwise, the subsequent then() method will not receive the promise object created by the preceding then() method.
  7. A then() method accepts two optional arguments: a successCallback function and a failureCallback function. For instance, then(successCallback, failureCallback).
  8. An alternate way of writing then(null, failureCallback) is catch(failureCallback).
  9. You can use a single catch() method to handle all errors in the preceding then() methods.
  10. You can chain additional callbacks to a catch() method. In other words, suppose you intend to perform a new task after a rejected promise. In that case, you can still chain new then() methods to a catch() method like so:
myMomsPromise
.then(successCallBack)
.catch(failureCallback)
.then(successCallBack)
  1. A catch() callback—which also returns a Promise—gets executed when the promise object chained to it is in the rejected state.
  2. The promise object returned by then() and catch() makes the methods chainable.
  3. A finally() callback gets executed, regardless of the success or failure of the promise object to which it is chained.
  4. Although then() is an inbuilt method of a Promise constructor, however, anyone can create one. Therefore, watch out for then() methods that are not inbuilt methods of the Promise constructor object. For instance, consider this example:
// Create a method named then:
const myThenMethod = {
then: function() {
console.log("I am a thenable object!");
},
};

// Invoke myThenMethod's then() method:
myThenMethod.then();

// The invocation above will return:
"I am a thenable object!"

Try it on StackBlitz

In the snippet above, myThenMethod is a thenable object but not a promise.

info

Thenable in JavaScript refers to any object that contains a then() method. Therefore, all promises are thenables. However, not all thenables are promises.

So, now that we know how to implement asynchronous programs in JavaScript, let's discuss some of the few advantages you will get by choosing to attach callbacks to a promise object.

Passing Callbacks into Functions vs. Attaching Callbacks to a Promise Object – Which Is the Best Way to Implement Asynchronous Programming?

You can see in the previous examples that passing callbacks into functions is barely readable—especially if you have numerous callbacks.

You are bound to create the callback pyramid of doom whenever you pass callbacks into functions—thereby making your codebase strenuous to read and debug.

Therefore, suppose you choose to implement async programming by attaching callbacks to your promise object. In that case, the main advantages you will benefit from are:

  • Your code will be readable and straightforward.
  • Your code will be easier to maintain and debug

Let's now discuss some of the Promise constructor's static methods that you can use to extend its functionality.

The Promise Constructor's Static Methods

The Promise constructor has some static methods that you can use to extends it functionality. Some of those methods are resolve(), reject(), all(), allSettled(), any(), and race().

Let's discuss each one, starting with resolve().

Promise.resolve(...)

Promise.resolve() returns a new promise object that resolves with the value passed as an argument to the resolve() method.

Keep in mind that Promise.resolve() is a simpler alternative to the new Promise(...) syntax—especially when you do not need to use the Promise constructor's function argument.

For instance, here's one way you can resolve a promise object to the "I will code sweetly!" string value.

const myPromise = new Promise(function (resolve, reject) {
resolve("I will code sweetly!");
});

console.log(myPromise);

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: "I will code sweetly!" }

A simpler way of writing the snippet above is by using Promise.resolve() like so:

const myPromise = Promise.resolve("I will code sweetly!");

console.log(myPromise);

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: "I will code sweetly!" }

Important stuff to know about Promise.resolve()

Keep these two essential pieces of info in mind whenever you choose to use Promise.resolve().

Info 1: Promise.resolve()'s argument determines the resolved value

A promise object's resolved value depends on the data type you passed as an argument to the resolve() method.

For instance, the promise object in the snippet below resolved to an array value because we passed in an array data type as an argument to the resolve() method.

const myPromise = Promise.resolve(["I will code sweetly"]);

console.log(myPromise);

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: Array ["I will code sweetly!"] }

Info 2: Promise.resolve() unwraps its argument

Promise.resolve() will always unwrap its argument down to its non-thenable value.

For instance, in the snippet below, we passed a real promise object to the first Promise.resolve() method. However, the Promise.resolve() method unwrapped the promise object down to its non-thenable value.

Therefore, the resultant promise object resolved to the "I will code sweetly!" string value.

const myPromise = Promise.resolve(
Promise.resolve(
Promise.resolve("I will code sweetly!")
)
);

console.log(myPromise);

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: "I will code sweetly" }

Here's another example:

// Create a thenable object:
const firstTest = {
then: function (fulfilled, rejected) {
rejected("Just a trial");
}
}

// Create another thenable object:
const secondTest = {
then: function (fulfilled) {
fulfilled(firstTest);
}
}

// Pass in secondTest's thenable object as an argument to the resolve() method:
Promise.resolve(secondTest);

// The invocation above will return:
Promise { <state>: "rejected", <reason>: "Just a trial" }

You can see that in the snippet above, we passed a thenable object as an argument to the Promise.resolve() method. However, the Promise.resolve() method unwrapped the thenable object down to its non-thenable value.

Therefore, the resultant promise object resolved to the "Just a trial" rejected string value.

Let's now talk about Promise.reject().

Promise.reject(...)

Promise.reject() returns a new promise object that is rejected based on the reason passed as an argument to the reject() method.

Keep in mind that Promise.reject() is a simpler alternative to the new Promise(...) syntax—especially when you do not need to use the Promise constructor's function argument.

For instance, here's one way you can reject a promise object to the string value of "Oops😲!".

const myPromise = new Promise(function (resolve, reject) {
reject("Oops😲!");
});

console.log(myPromise);

// The invocation above will return:
Promise { <state>: "rejected", <reason>: "Oops😲!" }

A simpler way of writing the snippet above is by using Promise.reject() like so:

const myPromise = Promise.reject("Oops😲!");

console.log(myPromise);

// The invocation above will return:
Promise { <state>: "rejected", <reason>: "Oops😲!" }

Important stuff to know about Promise.reject()

Keep these two essential pieces of info in mind whenever you choose to use Promise.reject().

Info 1: Promise.reject()'s argument determines the rejected reason

A promise object's rejected reason depends on the data type you passed as an argument to the reject() method.

For instance, the promise object below is rejected based on the reason of an array because we passed in an array data type as an argument to the reject() method.

const myPromise = Promise.reject(["Oops😲!"]);

console.log(myPromise);

// The invocation above will return:
Promise { <state>: "rejected", <reason>: Array [ "Oops😲!" ] }

Info 2: Promise.reject() does not unwrap its argument

Unlike the resolve() method, Promise.reject() will not unwrap its argument down to its non-thenable value.

Therefore, suppose you pass in a thenable value as an argument to the reject() method. In that case, Promise.reject() will use the thenable value as the rejected state's reason—without unwrapping it.

Here's an example:

const myPromise = Promise.reject({
then: function () {
return "Oops😲!";
},
});

console.log(myPromise);

// The invocation above will return:
Promise { <state>: "rejected", <reason>: Object { then: then() } }

You can see that in the snippet above, we passed a thenable object as an argument to the Promise.reject() method. However, the Promise.reject() method did not unwrap the thenable object down to its non-thenable value.

Therefore, the resultant promise object resolved to the rejected reason of the thenable object.

Let's now talk about Promise.all().

Promise.all([...])

Promise.all([...]) returns a new promise object that resolves to an array of all()'s iterable argument's values.

Promise.all([...]) will return a fulfilled promise if and when JavaScript fulfills all its iterable argument's values.

However, suppose JavaScript rejects any of the iterable's values. In that case, Promise.all([...]) will return a new promise object that is rejected based on the reason of the first iterable object's value that got rejected.

note
  • The all() method's iterable argument's items can be promises, thenables, or any of JavaScript's data types.
  • Promise.all() will automatically pass each of its iterable argument's items through Promise.resolve(). Therefore, all() will eventually resolve its iterable argument's values to a fully-fledged promise object.

Here's an example:

Promise.all(["Orange", Promise.resolve(24), 715]);

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: Array(3) ["Orange", 24, 715] }

You can see that the snippet above returned a new promise object that fulfilled to an array of all()'s array argument—because all the array's values got fulfilled.

Here's another example:

const item1 = Promise.resolve(24);
const item2 = 715;
const item3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Orange");
}, 30000);
});

Promise.all([item1, item2, item3]).then((val) => {
console.log(val);
});

// The invocation above will first return:
Promise { <state>: "pending" }

// Then after 30000 milliseconds (30 seconds) invocation will return:
Array(3) [24, 715, "Orange"]

The snippet above returned a new promise object that fulfilled to an array of all()'s array argument—because all the array's values got fulfilled.

Now, consider this third example:

Promise.all(["Orange", Promise.reject("Pinkmelon"), Promise.reject(24), 715]);

// The invocation above will return:
Promise { <state>: "rejected", <reason>: "Pinkmelon" }

Notice that the snippet above returned a new promise object rejected based on the reason of the first rejected array item.

But suppose you need your promise object to return after all the iterable's items get resolved—regardless of whether the values settled to a fulfilled or rejected state. In that case, use the Promise.allSettled() method. Let's talk about it below.

Promise.allSettled([...])

Promise.allSettled([...]) returns a new promise object that resolves after all allSettled()'s iterable argument's items have either been fulfilled or rejected.

note
  • The allSettled() method's iterable argument's items can be promises, thenables, or any of JavaScript's data types.
  • Promise.allSettled() will automatically pass each of its iterable argument's items through Promise.resolve(). Therefore, allSettled() will eventually resolve all its iterable argument's items to a fully-fledged promise object.

Here's an example:

Promise.allSettled(["Orange", Promise.resolve(24), 715]);

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: Array(3) [
{ status: "fulfilled", value: "Orange" },
{ status: "fulfilled", value: 24 },
{ status: "fulfilled", value: 715 }
]}

You can see that the snippet above returned a new promise object that resolved after all the allSettled()'s array argument's items got fulfilled.

Here's another example:

const item1 = Promise.resolve(24);
const item2 = 715;
const item3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Orange");
}, 30000);
});

Promise.allSettled([item1, item2, item3]).then((val) => {
console.log(val);
});

// The invocation above will first return:
Promise { <state>: "pending" }

// Then after 30000 milliseconds (30 seconds) invocation will return:
Array(3) [
{ status: "fulfilled", value: 24 },
{ status: "fulfilled", value: 715 }
{ status: "fulfilled", value: "Orange" },
]

You can see that the snippet above returned a new promise object that resolved after all the allSettled()'s array argument's items got fulfilled.

Now, consider this third example:

Promise.allSettled(["Orange", Promise.reject("Pinkmelon"), Promise.reject(24), 715]);

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: Array(4) [
{ status: "fulfilled", value: "Orange" },
{ status: "rejected", reason: "Pinkmelon" },
{ status: "rejected", reason: 24 }
{ status: "fulfilled", value: 715 }
]}

Notice that the snippet above returned a new promise object that resolved after all the allSettled()'s array argument's items got either fulfilled or rejected.

But what if you only need your promise object to return with only the item that first resolves to a fulfilled state? In that case, you would use the Promise.any() method. Let's talk about it below.

Promise.any([...])

Promise.any([...]) returns a new promise object that resolves with any()'s iterable argument's value that first settles to a fulfilled state.

However, suppose none of the items in the iterable object resolved to a fulfilled state. In that case, the any() method will return a new promise that is rejected with the reason of an AggregateError.

note
  • The any() method's iterable argument's items can be promises, thenables, or any of JavaScript's data types.
  • Promise.any() will automatically pass each of its iterable argument's items through Promise.resolve(). Therefore, any() will eventually resolve all its iterable argument's items to a fully-fledged promise object.

Here's an example:

Promise.any(["Orange", Promise.resolve(24), 715]);

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: "Orange"}

You can see that the snippet above returned a new promise object that resolved with any()'s array argument's value that first settled to a fulfilled state.

Here's another example:

const item1 = Promise.reject(24);
const item2 = Promise.reject(715);
const item3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Orange");
}, 30000);
});
const item4 = "Pinkmelon";

Promise.any([item1, item2, item3, item4]).then((val) => {
console.log(val);
});

// The invocation above will return:
"Pinkmelon"

Try it on StackBlitz

Among the four promise items of the array we passed into the any() method, notice that only the first fulfilled promise's value got returned.

Now, consider this third example:

Promise.any([Promise.reject("Pinkmelon"), Promise.reject(24)]);

// The invocation above will return:
Promise { <state>: "rejected", <reason>: AggregateError }

Notice that the snippet above returned a new promise object that is rejected based on the reason of an AggregateError because all the array argument's items resolved to a rejected state.

But what if you only need your promise object to return with only the item that JavaScript first settled—irrespective of whether JavaScript resolved it to a fulfilled or rejected state? In that case, you would use the Promise.race() method. Let's talk about it below.

Promise.race([...])

Promise.race([...]) returns a new promise object that resolves with race()'s iterable argument's value that first got settled to either a fulfilled or rejected state.

note
  • The race() method's iterable argument's items can be promises, thenables, or any of JavaScript's data types.
  • Promise.race() will automatically pass each of its iterable argument's items through Promise.resolve(). Therefore, race() will eventually resolve all its iterable argument's items to a fully-fledged promise object.

Here's an example:

Promise.race(["Orange", Promise.resolve(24), 715]);

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: "Orange"}

You can see that the snippet above returned a new promise object that resolved with race()'s array argument's value that first got settled.

Here's another example:

const item1 = Promise.reject(24);
const item2 = Promise.reject(715);
const item3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Orange");
}, 30000);
});
const item4 = "Pinkmelon";

Promise.race([item1, item2, item3, item4]).then((val) => {
console.log(val);
});

// The invocation above will return:
Promise { <state>: "rejected", <reason>: 24 }

You can see that the snippet above returned a new promise object that resolved with race()'s array argument's value that first got settled.

Now, consider this third example:

{
const item1 = new Promise((resolve, reject) => {
setTimeout(resolve, 8000, "Orange");
});

const item2 = new Promise((resolve, reject) => {
setTimeout(reject, 3000, "Banana");
});

const item3 = new Promise((resolve, reject) => {
setTimeout(resolve, 50000, "Grape");
});

Promise.race([item1, item2, item3]).then((val) => {
console.log(val);
});
}

// The invocation above will first return:
Promise { <state>: "pending" }

// Then, after 3000 milliseconds (3 seconds), the invocation will return:
"Uncaught (in promise) Banana"

Notice that the snippet above returned the value of the promise object that JavaScript first settled.

Keep in mind that race() will remain pending—forever—if you pass an empty array argument into it.

For instance, consider this code:

Promise.race([]);

// The invocation above will return:
Promise { <state>: "pending" }

The snippet above will never get settled (fulfilled or rejected) because we used an empty array as race()'s argument.

So, now that we know how to use the Promise constructor's static methods to extend Promise's functionality, we can talk about an easier way to write promises.

Async and Await: An Easier Way to Implement Asynchronous Programming

The async and await keywords provide us with an alternate—and easier—way to write and read promises.

Let's see how the two keywords work.

async keyword

When you place the async keyword before a regular function declaration, the keyword will turn that function into an asynchronous function.

By default, an async function returns a promise object that is either resolved with the function's returned value or rejected based on the exception thrown.

In other words, the async keyword will automatically pass an asynchronous function's return value through the Promise.resolve() static method.

Examples

The function below is a regular function that will return a regular string value.

function myMomsPromise() {
return "I get a book";
}

// Invoke the myMomsPromise() asynchronous function:
console.log(myMomsPromise());

// The invocation above will return:
"I get a book"

However, placing the async keyword before the function declaration will turn the function into an asynchronous function.

Therefore, the function will return a promise object instead of a string.

async function myMomsPromise() {
return 'I get a book';
}

// Invoke the myMomsPromise() asynchronous function:
console.log(myMomsPromise());

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: "I get a book" }

The async keyword instructed the function to return a promise object rather than outputting the string value directly.

The async function above is similar to writing:

function myMomsPromise() {
return Promise.resolve('I get a book');
}

// Invoke the myMomsPromise() asynchronous function:
console.log(myMomsPromise());

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: "I get a book" }

You can use the async keyword with function expressions and arrow functions.

// Example of an async function expression:
const myMomsPromise = async function() { return "I get a book" };

// Example of an async arrow function expression:
const myMomsPromise = async () => "I get a book";

Now, take a look at the Promise constructor equivalence of the async function:

const myMomsPromise = new Promise(function (resolve, reject) {
resolve("I get a book");
});

// Check myMomsPromise's content:
console.log(myMomsPromise);

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: "I get a book" }

Keep in mind that an async function is simply a syntactic sugar that makes reading and writing promises easier.

In other words, an async function is a simplified way of writing promises. Therefore, you can still use it to create a promise chain.

Here's an example:

async function myMomsPromise() {
return 'I get a book';
}

// Promise chain:
myMomsPromise()
.then(value => value + " " + "and a car")
.then(value => console.log(value))
.catch(e => console.error(e));

// The invocation above will return:
"I get a book and a car"

Try it on StackBlitz

So, now that we know what an async keyword does, let's discuss the await keyword.

await keyword

The await keyword instructs a function to wait for a promise to be settled before continuing its execution.

note
  • The await keyword works only inside an async function in a regular JavaScript code. However, in a JavaScript module, you can use await on its own—that is, outside an async function.
  • You can use zero or more await expressions in an async function.

Examples

The await keyword in the snippet below instructs showMomsPromise to wait for myMomsPromise to be settled before continuing its execution.

async function showMomsPromise() {
const myMomsPromise = new Promise(function (resolve, reject) {
setTimeout(resolve, 5000, "I get a book");
});
console.log(await myMomsPromise);
}

// Invoke the showMomsPromise() asynchronous function:
showMomsPromise();

// The invocation above will first return:
Promise { <state>: "pending" }

// Then, after 5000 milliseconds (5 seconds), the invocation will return:
"I get a book"

Suppose we had omitted the await keyword in the snippet above. In such a case, the console.log() statement would have invoked myMomsPromise immediately—without waiting for its promise to be resolved.

Consequently, showMomsPromise()'s invocation would have returned a fulfilled promise that is resolved with an undefined value.

Here's an example:

In the snippet below, JavaScript invoked the console.log(myMomsPromise) statement immediately because it has no await keyword.

async function showMomsPromise() {
const myMomsPromise = new Promise(function (resolve, reject) {
setTimeout(resolve, 5000, 'I get a book');
});
console.log(myMomsPromise);
}

// Invoke the showMomsPromise() asynchronous function:
showMomsPromise();

// The invocation above will first return:
Promise { <state>: "pending" }

// Immediately afterwards, the invocation will return:
Promise { <state>: "fulfilled", <value>: undefined }

An async function will always return a promise—even if you do not return any value.

async function showMomsPromise() {}

// Invoke the showMomsPromise() asynchronous function:
showMomsPromise();

// The invocation above will return:
Promise { <state>: "fulfilled", <value>: undefined }

You can see that the async function above returned a promise that resolved with an undefined value because we did not explicitly tell it to return any item.

Here's another example:

async function showMomsPromise() {
const myMomsPromise = await "I get a book";
console.log(myMomsPromise);
}

// Invoke the showMomsPromise() asynchronous function:
showMomsPromise();

// The invocation above will first return:
"I get a book"

// Then, it will also return:
Promise { <state>: "fulfilled", <value>: undefined }

The async function in the snippet above returned a promise with an undefined value because we did not tell it to return any value. Instead, we only told it to log "I get a book" to the browser's console.

Here's a third example:

async function showMomsPromise() {
const myMomsPromise = await "I get a book";
console.log(myMomsPromise);
return myMomsPromise;
}

// Invoke the showMomsPromise() asynchronous function:
showMomsPromise();

// The invocation above will first return:
"I get a book"

// Then, it will also return:
Promise { <state>: "fulfilled", <value>: "I get a book" }

You can now see that the async function above returned a promise object that fulfilled with the function's output.

So, now that you know how the async and await keywords work, let's convert a then() method snippet we saw previously into an async/await code.

How to Convert a then() Promise Code to an Async/Await Program

Remember that we used the snippet below previously to represent mom's promise and its eventual fulfillment (or rejection).

Take some time to review the code.

// Create two variables:
let finalResponse = null;
const passedExam = true;

// Create a function that return mom's first promise:
function emitFirstProm() {
return "I get a book";
}

// Create a function that add mom's first promise to her second one:
function emitSecondProm(firstPromise) {
return firstPromise + ", a phone";
}

// Create a function that add mom's previous promises to her third one:
function emitThirdProm(pastProms) {
return pastProms + ", a laptop";
}

// Create a function that add mom's previous promises to her fourth one:
function emitFourthProm(pastProms) {
return pastProms + ", and a Jeep 🤩🎉✨";
}

// Create mom's promise:
const myMomsPromise = new Promise(function (resolve, reject) {
if (passedExam) {
resolve(emitFirstProm());
} else {
reject("You did not pass your exam, so mom rejected her promise.");
}
});

// Invoke each of the functions above asynchronously by attaching them to myMomsPromise object:
myMomsPromise
.then(emitSecondProm)
.then(emitThirdProm)
.then(emitFourthProm)
.then(function (value) {
finalResponse = `Mom fulfilled her promise! ${value}`;
})
.catch(function (e) {
console.error(`Oops! Caught the following error: ${e}`);
})
.finally(function () {
console.log(finalResponse);
});

// The invocation above will return:
"Mom fulfilled her promise! I get a book, a phone, a laptop, and a Jeep 🤩🎉✨"

Try it on StackBlitz

The snippet above used a chain of then() methods to attach callbacks to the myMomsPromise object.

We can make the snippet much simpler and easier to read by replacing the then() blocks with async/await like so:

// Create two variables:
let finalResponse = null;
const passedExam = true;

// Create a function that return mum's first promise:
function emitFirstProm() {
return "I get a book";
}

// Create a function that add mum's first promise to her second one:
function emitSecondProm(firstPromise) {
return firstPromise + ", a phone";
}

// Create a function that add mum's previous promises to her third one:
function emitThirdProm(pastProms) {
return pastProms + ", a laptop";
}

// Create a function that add mum's previous promises to her fourth one:
function emitFourthProm(pastProms) {
return pastProms + ", and a Jeep 🤩🎉✨";
}

// Create mom's promise:
const myMomsPromise = async function () {
if (passedExam) {
const firstPromise = await emitFirstProm();
const secondPromise = await emitSecondProm(firstPromise);
const thirdPromise = await emitThirdProm(secondPromise);
const fourthPromise = await emitFourthProm(thirdPromise);
finalResponse = `Mom fulfilled her promise! ${fourthPromise}`;
} else {
throw new Error("You did not pass your exam, so mom rejected her promise.");
}
};

// Invoke the myMomsPromise async function:
myMomsPromise()
.catch(function (e) {
console.error(`Oops! Caught the following error: ${e}`);
})
.finally(function (e) {
console.log(finalResponse);
});

// The invocation above will return:
"Mom fulfilled her promise! I get a book, a phone, a laptop, and a Jeep 🤩🎉✨"

Try it on StackBlitz

You can see in the snippet above that we no longer need a chain of then() blocks. Instead, we used the await keyword before each function call.

Then, we assigned the returned promise objects to the firstPromise, secondPromise, thirdPromise, and fourthPromise variables, respectively.

note

Each await keyword instructs myMomsPromise to pause its execution until the keyword's function call returns its result. Therefore, all subsequent code after the paused line will not execute until await's function call returns its value.

You can also use a hybrid of the then() block, and the async/await keywords to make your code more flexible.

Here's an example:

// Create two variables:
let finalResponse = null;
const passedExam = true;

// Create a function that return mum's first promise:
const emitFirstProm = () => "I get a book";

// Create a function that add mum's first promise to her second one:
const emitSecondProm = firstPromise => firstPromise + ", a phone";

// Create a function that add mum's previous promises to her third one:
const emitThirdProm = pastProms => pastProms + ", a laptop";

// Create a function that add mum's previous promises to her fourth one:
const emitFourthProm = pastProms => pastProms + ", and a Jeep 🤩🎉✨";

// Create mom's promise:
const myMomsPromise = async function () {
if (passedExam) {
const firstPromise = await emitFirstProm();
const secondPromise = await emitSecondProm(firstPromise);
const thirdPromise = await emitThirdProm(secondPromise);
const fourthPromise = await emitFourthProm(thirdPromise);
return `Mom fulfilled her promise! ${fourthPromise}`;
} else {
throw new Error("You did not pass your exam, so mom rejected her promise.");
}
};

// Invoke the myMomsPromise async function:
myMomsPromise()
.then(value => finalResponse = value)
.catch(e => console.error(`Oops! Caught the following error: ${e}`))
.finally(() => console.log(finalResponse));

// The invocation above will return:
"Mom fulfilled her promise! I get a book, a phone, a laptop, and a Jeep 🤩🎉✨"

Try it on StackBlitz

Before we wrap up our discussion on asynchronous programming, you should be aware of how an async function's execution order works. So, let's talk about that below.

How Does an Async Function's Execution Order Work?

Consider the following MDN's statements:

The body of an async function can be thought of as being split by zero or more await expressions.

Top-level code, up to and including the first await expression (if there is one), is run synchronously. In this way, an async function without an await expression will run synchronously.

If there is an await expression inside the function's body, however, the async function will always complete asynchronously.

Code after each await expression can be thought of as existing in a .then callback. In this way a promise chain is progressively constructed with each reentrant step through the function. The return value forms the final link in the chain.

MDN

Let's use an example to understand the statements above.

Example of How an Async Function's Execution Order Works

Consider this snippet below:

function myMomsPromise() {
console.log("Starting mom's promise check...");
return new Promise(resolve => {
setTimeout(function () {
resolve("I get a book.");
console.log("Mom's promise is fulfilled!");
}, 5000);
});
}

function myDadsPromise() {
console.log("Starting dad's promise check...");
return new Promise(resolve => {
setTimeout(function () {
resolve("I get a pen.");
console.log("Dad's promise is fulfilled!");
}, 1000);
});
}

async function checkMomAndDadsPromises() {
console.log("===Promises confirmation check started===");

const momsPromiseResponse = await myMomsPromise();
console.log(momsPromiseResponse);

const dadsPromiseResponse = await myDadsPromise();
console.log(dadsPromiseResponse);
}

function startPromiseChecks() {
checkMomAndDadsPromises();
console.log("I prayed while waiting for mom's promise.");
console.log("Mom's promise is worth waiting for.");
setTimeout(() => console.log("Is mom's promise still pending?"), 5100);
}

startPromiseChecks();

Here's what we did in the snippet above:

  1. We use a regular function (startPromiseChecks) to run an async function (checkMomAndDadsPromises)
  2. We programmed the checkMomAndDadsPromises async function to await two promises (myMomsPromise and myDadsPromise) successively.

Now, when startPromiseChecks() gets invoked, in what order do you think each of startPromiseChecks' code would execute? Let's find out.

  1. startPromiseChecks will first invoke the checkMomAndDadsPromises async function.
  2. checkMomAndDadsPromises will start its execution synchronously until it reaches its first await expression's pending promise.
  3. On getting to its first await expression, checkMomAndDadsPromises will:
    1. Log "===Promises confirmation check started===" and "Starting mom's promise check..." to the browser's console.
    2. Wait for myMomsPromise's resolution—while also yielding control back to startPromiseChecks (the function that invoked checkMomAndDadsPromises).
  4. While awaiting myMomsPromise's settlement, startPromiseChecks—which now has the control—will continue executing its code.
  5. After a while (5000 milliseconds), when myMomsPromise gets settled, checkMomAndDadsPromises will regain control. Therefore, it will:
    1. Assign the first await expression's resolution to momsPromiseResponse.
    2. Log the momsPromiseResponse's value to the browser's console.
    3. Evaluates its second await expression
    4. Log "Starting dad's promise check..." to the browser's console.
    5. Wait for myDadsPromise's resolution—while also yielding control back to startPromiseChecks (the function that invoked checkMomAndDadsPromises).
  6. While awaiting myDadsPromise's settlement, startPromiseChecks—which now has the control—will continue executing its code.
  7. After a while (1000 milliseconds), when myDadsPromise gets settled, checkMomAndDadsPromises will regain control. Therefore, it will:
    1. Assign the second await expression's resolution to dadsPromiseResponse.
    2. Log the dadsPromiseResponse's value to the browser's console.

After logging dadsPromiseResponse's value to the browser's console, if any return expression exists inside the checkMomAndDadsPromises function, the computer will yield control to the expression.

However, none exist in our case, so startPromiseChecks()'s invocation output will look like so:

===Promises confirmation check started===
Starting mom's promise check...
I prayed while waiting for mom's promise.
Mom's promise is worth waiting for.
undefined
Mom's promise is fulfilled!
I get a book.
Starting dad's promise check...
Is mom's promise still pending?
Dad's promise is fulfilled!
I get a pen.

Try it on StackBlitz

Wrapping It Up

This article discussed how asynchronous programming works in JavaScript. We also learned how to implement async programming with callbacks and promises.

And lastly, we saw how async and await keywords make it easier to write promises.

I hope this article has helped you grasp asynchronous JavaScript programming better.

Thanks for reading!

Useful Resource