In this tutorial, you will learn everything you need to know about using promises and async/await in JavaScript.
So let's get started.
Why Use Promises in JavaScript?
ES6 introduced promises as a native implementation. Before ES6 we were using callbacks to handle asynchronous operations.
Let’s understand what callbacks are and what problem related to callbacks is solved by promises.
Let's say we have a list of posts and their respective comments, like this:
const posts = [
{ post_id: 1, post_title: 'First Post' },
{ post_id: 2, post_title: 'Second Post' },
{ post_id: 3, post_title: 'Third Post' },
];
const comments = [
{ post_id: 2, comment: 'Great!'},
{ post_id: 2, comment: 'Nice Post!'},
{ post_id: 3, comment: 'Awesome Post!'},
];
Now, we will write a function to get a post by passing the post id. If the post is found, we will retrieve the comments related to that post.
const getPost = (id, callback) => {
const post = posts.find( post => post.post_id === id);
if(post) {
callback(null, post);
} else {
callback("No such post found", undefined);
}
};
const getComments = (post_id, callback) => {
const result = comments.filter( comment => comment.post_id === post_id);
if(result) {
callback(null, result);
} else {
callback("No comments found", undefined);
}
}
In the above getPost
and getComments
functions, if there is an error we will pass it as the first argument to the callback function. But if we get the result, we will call the callback function and pass the result as the second argument to it.
If you are familiar with Node.js, then you will know that this is a very common pattern used in every Node.js callback function.
Now let’s use those functions:
getPost(2, (error, post) => {
if(error) {
return console.log(error);
}
console.log('Post:', post);
getComments(post.post_id, (error, comments) => {
if(error) {
return console.log(error);
}
console.log('Comments:', comments);
});
});
After executing the above code, you will see the following output:
Here's a CodePen Demo.
As you can see, we have the getComments
function nested inside the getPost
callback.
Now imagine if we also wanted to find the likes of those comments. That would also get nested inside getComments
callback, creating more nesting. This will eventually make the code difficult to understand.
This nesting of the callbacks is known as callback hell.
You can see that the error handling condition also gets repeated in the code, which creates duplicate code – this is not good.
So to fix this problem and allow asynchronous operations, promises were introduced.
What are Promises in JavaScript?
Promises are one of the most important parts of JavaScript – but they can be confusing and difficult to understand. Many new devs, as well as experienced ones, struggle to fully grasp them.
So what is a promise? A promise represents an asynchronous operation whose result will come in the future.
Before ES6, there was no way to wait for something to perform some operation. For example, when we wanted to make an API call, there was no way to wait until the results came back.
For that, we used to use external libraries like JQuery or Ajax which had their own implementation of promises. But there was no JavaScript implementation of promises.
But then promises were added in ES6 as a native implementation. And now, using promises in ES6, we can make an API call ourselves and wait until it's done to perform some operation.
How to Create a Promise
To create a promise we need to use the Promise
constructor function like this:
const promise = new Promise(function(resolve, reject) {
});
The Promise
constructor takes a function as an argument and that function internally receives resolve
and reject
as parameters.
The resolve
and reject
parameters are actually functions that we can call depending on the outcome of the asynchronous operation.
A Promise
can go through three states:
Pending
Fulfilled
Rejected
When we create a promise, it’s in a pending state. When we call the resolve
function, it goes in a fulfilled state, and if we call reject
it will go into the rejected state.
To simulate the long-running or asynchronous operation, we will use the setTimeout
function.
const promise = new Promise(function(resolve, reject) {
setTimeout(function() {
const sum = 4 + 5;
resolve(sum);
}, 2000);
});
Here, we've created a promise which will resolve to the sum of 4
and 5
after a 2000ms (2 second) timeout is over.
To get the result of the successful promise execution, we need to register a callback handler using .then
like this:
const promise = new Promise(function(resolve, reject) {
setTimeout(function() {
const sum = 4 + 5;
resolve(sum);
}, 2000);
});
promise.then(function(result) {
console.log(result); // 9
});
So whenever we call resolve
, the promise will return back the value passed to the resolve
function which we can collect using the .then
handler.
If the operation is not successful, then we call the reject
function like this:
const promise = new Promise(function(resolve, reject) {
setTimeout(function() {
const sum = 4 + 5 + 'a';
if(isNaN(sum)) {
reject('Error while calculating sum.');
} else {
resolve(sum);
}
}, 2000);
});
promise.then(function(result) {
console.log(result);
});
Here, if the sum
is not a number, then we call the reject
function with the error message. Otherwise, we call the resolve
function.
If you execute the above code, you will see the following output:
As you can see, we're getting an uncaught error message along with the message we've specified because calling the reject
function throws an error. But we have not added an error handler for catching that error.
To catch the error, we need to register another callback using .catch
like this:
promise.then(function(result) {
console.log(result);
}).catch(function(error) {
console.log(error);
});
You will see the following output:
As you can see, we have added the .catch
handler, so we're not getting any uncaught error – we're just logging the error to the console.
This also avoids stopping your application abruptly.
So it's always recommended to add the .catch
handler to every promise so your application will not stop running because of the error.
When to Use resolve and reject
Let's take an example of an API call.
If you're making an API call and the API call is successful, then you call the resolve
function by passing the result of the API as an argument to it.
And if the API is unsuccessful, then you call the reject
function by passing any message as an argument to it.
So to indicate that the operation is successful, we call the resolve
function and for indicating an unsuccessful operation, we call the reject
function.
What is Promise Chaining and Why is it Useful?
Promise chaining is a technique used to manage asynchronous operations in a more organized and readable way.
In promise chaining, we can attach multiple .then
handlers in which result of previous .then
handler is automatically passed to the next .then
handler.
Using promise chaining helps to avoid the problem of callback hell which we have seen previously.
Promise chaining also allows us to write asynchronous code in a more linear and sequential manner, which is easier to read and understand.
Also, when using promise chaining, we can attach only one.catch
handler at the end of all the .then
handlers. If any of the in-between promises fail, the last .catch
handler will be automatically executed.
So we don't need to add multiple .catch
handlers. This eliminates multiple error check as we did in the callback hell example previously.
How Promise Chaining Works
We can add multiple .then
handlers to a single promise like this:
promise.then(function(result) {
console.log('first .then handler');
return result;
}).then(function(result) {
console.log('second .then handler');
console.log(result);
}).catch(function(error) {
console.log(error);
});
When we have multiple .then
handlers added, the return value of the previous .then
handler is automatically passed to the next .then
handler.
As you can see, adding 4 + 5
resolves a promise and we get that sum in the first .then
handler. There we're printing a log statement and return that sum to the next .then
handler.
And inside the next .then
handler, we're adding a log statement and then we're printing the result we got from the previous .then
handler.
This way of adding multiple .then
handlers is known as promise chaining.
How to Delay a Promise's Execution in JavaScript
Many times we don't want the promise to execute immediately. Rather, we want it to delay until after some operation is completed.
To achieve this, we can wrap the promise in a function and return that promise from that function like this:
function createPromise() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
const sum = 4 + 5;
if(isNaN(sum)) {
reject('Error while calculating sum.');
} else {
resolve(sum);
}
}, 2000);
});
}
This way, we can use the function parameters inside the promise, making the function truly dynamic.
function createPromise(a, b) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
const sum = a + b;
if(isNaN(sum)) {
reject('Error while calculating sum.');
} else {
resolve(sum);
}
}, 2000);
});
}
createPromise(1,8)
.then(function(output) {
console.log(output); // 9
});
// OR
createPromise(10,24)
.then(function(output) {
console.log(output); // 34
});
Note: When we create a promise, it will be either resolved or rejected but not both at the same time. So we cannot add two resolve
or reject
function calls in the same promise.
Also, we can pass only a single value to the resolve
or reject
function.
If you want to pass multiple values to a resolve
function, pass it as an object like this:
const promise = new Promise(function(resolve, reject) {
setTimeout(function() {
const sum = 4 + 5;
resolve({
a: 4,
b: 5,
sum
});
}, 2000);
});
promise.then(function(result) {
console.log(result);
}).catch(function(error) {
console.log(error);
});
How to Use Arrow Functions in JavaScript
In all the above code examples, we've used regular ES5 function syntax while creating promises.
But it's a common practice to use arrow function syntax instead of ES5 function syntax.
So let's first understand what is an arrow function and how to use it.
What are arrow functions?
Before ES6, there were two main ways of declaring functions.
1. Function Declaration Syntax:
function add(a, b) {
return a + b;
}
2. Function Expression Syntax:
const add = function(a, b) {
return a + b;
};
The main visible difference between the regular function and arrow function is the syntax of writing the function.
Using arrow function syntax, we can write the above adding function like this:
const add = (a, b) => {
return a + b;
};
You might not see much difference here, apart from the arrow. But if we have a single line of code in the function body we can simplify the above arrow function like this:
const add = (a, b) => a + b;
Here we are implicitly returning the result of a + b
, so there is no need for a return
keyword if there is a single statement.
So using the arrow functions will make your code much shorter.
If you want to learn other features of arrow functions, you can check out this video.
Using an arrow function, we can write the previous code as shown below:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const sum = 4 + 5 + 'a';
if(isNaN(sum)) {
reject('Error while calculating sum.');
} else {
resolve(sum);
}
}, 2000);
});
promise.then((result) => {
console.log(result);
});
You can either use ES5 or ES6 function syntax depending on your preferences and needs.
How to Use Async/Await in JavaScript
In this section, we'll explore everything you need to know about async/await.
In this section, we'll explore everything you need to know about async/await.
Async/await gives developers a better way to use promises.
To use async/await, you need to create a function and add the async
keyword before the function name using ES5 function declaration syntax like this:
async function someFunction() {
// function body
}
or using function expression syntax like this:
const someFunction = async function () {
// function body
};
or using an arrow function like this:
const someFunction = async () => {
// function body
};
Always remember that, when you add the async keyword to the function, it always returns a promise.
Take a look at the below code:
const sayHello = async function () {
return 'Hello';
};
sayHello();
What do you think the output of the above code will be?
The output is a promise fulfilled with the string Hello
.
So the below code:
const sayHello = async function () {
return 'Hello';
};
is the same as this:
const sayHello = function() {
return new Promise((resolve, reject) => {
resolve('Hello');
});
}
which is the same as this:
const sayHello = function () {
return Promise.resolve('Hello');
};
Promise.resolve('Hello')
is just a shorter way of creating a promise which resolves to the string Hello
.
So to get the actual string Hello
, we need to add the .then
handler like this:
sayHello().then(function (result) {
console.log(result); // Hello
});
Promise.resolve('Hello')
is just a shorter way of creating a promise which resolves to string Hello
.
So to get the actual string Hello
, we need to add .then
handler like this:
Now, where do we use the await
keyword?
It's used inside the function which is declared as async
. So the await
keyword should only be used inside the async
function.
You will get an error if you try to use it in non-async functions.
Suppose, we have a promise which returns the product of two numbers like this:
function getProduct(a, b) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(a * b);
}, 1000);
});
}
and we're using it like this:
getProduct(2, 4)
.then(function (result) {
getProduct(result, 2)
.then(function (finalResult) {
console.log('final_result', finalResult);
})
.catch(function (error) {
console.log(error);
});
})
.catch(function (error) {
console.log(error);
});
In the above code, we're first getting the product of 2
and 4
. Then we're using that result to multiply it by 2
again, and then finally printing the product.
If you execute the above code, you will see the final result as 16 which is 2 * 4 = 8 and 8 * 2 = 16.
The above code of .then
and .catch
looks pretty complicated and difficult to understand at one glance.
So using async/await we can simplify the above code to this:
const printResult = async () => {
try {
const result = await getProduct(2, 4); // line 1
const finalResult = await getProduct(result, 2); // line 2
console.log('final_result', finalResult); // line 3
} catch (error) {
console.log(error);
}
};
printResult();
This looks much cleaner and easy to understand.
Here, to use the await
keyword, we're declaring a function with the async
keyword. Then to get the result of each promise, we're adding the await
keyword in front of it.
Also, note that we've added try/catch inside the function. You always need to add a try block around the code which uses await
so the catch block will be executed if the promise gets rejected.
There is a very important thing you need to remember: The above async/await code will work exactly the same as when we use .then
– so the next await
line (line 2) will not be executed until the previous await
call (line 1) is successful.
Therefore, as the getProduct
function is taking 1 second to execute because of the setTimeout call, line 2 will have to wait for 1 second before executing the getProduct
function again.
But there is one exception to this behavior, which you can check out in this article.
Also, if there is an error while executing line 1 (because of some error that occurred in the getProduct
function), the next code after line 1 will not be executed. Instead, the catch block will be executed.
Now, if you compare the code of promise chaining and async/await, you will see the difference.
// code using async/await
const printResult = async () => {
try {
const product = await getProduct(2, 4); // line 1
const finalResult = await getProduct(product, 2); // line 2
console.log('final_result', finalResult); // line 3
} catch (error) {
console.log(error);
}
};
printResult();
// code using .then and .catch
getProduct(2, 4)
.then(function (result) {
getProduct(result, 2)
.then(function (finalResult) {
console.log('final_result', finalResult);
})
.catch(function (error) {
console.log(error);
});
})
.catch(function (error) {
console.log(error);
});
As the nesting gets deeper, the code using promise chaining gets more complicated so async/await just provides a way to write the same code but with better clarity.
Using async/await also reduces the need of adding multiple .catch
handlers to handle the errors.
We can certainly avoid the nesting in the above promise chaining by writing the previous code like this:
getProduct(2, 4)
.then(function (result) {
return getProduct(result, 2);
})
.then(function (finalResult) {
console.log('final_result', finalResult);
})
.catch(function (error) {
console.log(error);
});
Here, from the first .then
handler, we're returning the result of getProduct(result, 2)
.
Whatever returned from the previous .then
handler will be passed to the next .then
handler.
As the getProduct
function returns a promise so we can attach .then
again to it and avoid the need for a nested .catch
handler.
But still async/await syntax looks cleaner and easier to understand than the promise chaining syntax.
Promise Methods
In this section, we'll explore the various methods provided by the Promise API.
All these methods are useful when you want to execute multiple asynchronous tasks at the same time when those tasks are not dependent on each other (which saves a lot of time).
Because if you execute each task one after the other, then you have to wait for the previous task to finish before you can start with the next task.
And if the tasks are not related to each other, there is no point in waiting for the previous task to finish before executing the next task.
The Promise.all
method
This method is used to execute multiple asynchronous tasks simultaneously without having to wait for another task to finish.
Suppose we have three promises and all are resolved successfully:
const promise1 = new Promise((resolve, reject) => resolve('promise1 success'));
const promise2 = new Promise((resolve, reject) => resolve('promise2 success'));
const promise3 = new Promise((resolve, reject) => resolve('promise3 success'));
Now, let's use the Promise.all
method.
Promise.all
needs an array of promises as its argument.
Promise.all([promise1, promise2, promise3])
.then((result) => {
console.log('resolved', result); // resolved ["promise1 success", "promise2 success", "promise3 success"]
})
.catch((error) => {
console.log('rejected', error);
});
As all the promises are resolved, result
will be an array containing the results of the resolved promises.
Now, what if any of the promises get rejected?
const promise1 = new Promise((resolve, reject) => resolve('promise1 success'));
const promise2 = new Promise((resolve, reject) => reject('promise2 failure'));
const promise3 = new Promise((resolve, reject) => resolve('promise3 success'));
Promise.all([promise1, promise2, promise3])
.then((result) => {
console.log('resolved', result);
})
.catch((error) => {
console.log('rejected', error); // rejected promise2 failure
});
In the above code, promise2 is rejected so the catch handler will be executed, and in the case of Promise.all
:
If one of the promises is rejected, the
error
will contain the error message of the failed promise (as in our case above)If multiple promises are rejected, the
error
will be the error message of the first failed promise.
Note: Even though the intermediate promise gets rejected, all next promises will not be stopped from executing. They will all be executed – but only the first rejected promise value will be available in the error parameter of the catch block.
The Promise.race
method
Consider again the three resolved promises:
const promise1 = new Promise((resolve, reject) => resolve('promise1 success'));
const promise2 = new Promise((resolve, reject) => resolve('promise2 success'));
const promise3 = new Promise((resolve, reject) => resolve('promise3 success'));
Promise.race([promise1, promise2, promise3])
.then((result) => {
console.log('resolved', result); // resolved promise1 success
})
.catch((error) => {
console.log('rejected', error);
});
As you can see here, as soon as the first promise gets resolved, the Promise.race
method will return the result of that resolved promise.
Now, take a look at the below code:
const promise1 = new Promise((resolve, reject) => reject('promise1 failure'));
const promise2 = new Promise((resolve, reject) => resolve('promise2 success'));
const promise3 = new Promise((resolve, reject) => resolve('promise3 success'));
Promise.race([promise1, promise2, promise3])
.then((result) => {
console.log('resolved', result);
})
.catch((error) => {
console.log('rejected', error); // rejected promise1 failure
});
As you can see here, the first promise itself is rejected so the .catch
handler will be executed.
So when we use the Promise.race
method, it will wait until the first promise gets resolved or rejected and then:
If the first promise in the promise chain gets resolved, the
.then
handler will be executed and the result will be the result of the first resolved promise.If the first promise in the promise chain gets rejected, the
.catch
handler will be executed and the result will be the result of the first failed promise.If multiple promises are rejected, the
.catch
handler will be executed and the result will be the result of the first failed promise.
The Promise.allSettled
method
This method is useful when you want to know the result of each task even though they are rejected.
Because in Promise.all
and Promise.race
, we get only the result of the first rejected promise and there is no way to get the result of other successful or failed promises.
So using Promise.allSettled
we can get the result of all the promises, even if they failed.
Take a look at the below code:
const promise1 = new Promise((resolve, reject) => resolve('promise1 success'));
const promise2 = new Promise((resolve, reject) => resolve('promise2 success'));
const promise3 = new Promise((resolve, reject) => resolve('promise3 success'));
Promise.allSettled([promise1, promise2, promise3]).then((result) => {
console.log('resolved', result);
});
/* output from `.then`:
resolved [
{
"status": "fulfilled",
"value": "promise1 success"
},
{
"status": "fulfilled",
"value": "promise2 success"
},
{
"status": "fulfilled",
"value": "promise3 success"
}
]
*/
As you can see, the Promise.allSettled
method waits until all the promises are resolved or rejected and the result
will contain the result of each promise.
const promise1 = new Promise((resolve, reject) => reject('promise1 failure'));
const promise2 = new Promise((resolve, reject) => resolve('promise2 success'));
const promise3 = new Promise((resolve, reject) => resolve('promise3 success'));
Promise.allSettled([promise1, promise2, promise3]).then((result) => {
console.log('resolved', result);
});
/* output from `.then`:
resolved [
{
"status": "rejected",
"reason": "promise1 failure"
},
{
"status": "fulfilled",
"value": "promise2 success"
},
{
"status": "fulfilled",
"value": "promise3 success"
}
]
*/
In the above case, even though the first promise is rejected, we get the result of all the promises inside the .then
handler.
const promise1 = new Promise((resolve, reject) => reject('promise1 failure'));
const promise2 = new Promise((resolve, reject) => reject('promise2 failure'));
const promise3 = new Promise((resolve, reject) => reject('promise3 failure'));
Promise.allSettled([promise1, promise2, promise3]).then((result) => {
console.log('resolved', result);
});
/* output from `.then`:
resolved [
{
"status": "rejected",
"reason": "promise1 failure"
},
{
"status": "rejected",
"reason": "promise2 failure"
},
{
"status": "rejected",
"reason": "promise3 failure"
}
]
*/
Here, even though all the promises are rejected, still the .then
handler will be executed and we get the result of each promise.
Want to learn how to use these promise methods in an actual React application? Check out my previous article.
Thanks for Reading!
That's it for this tutorial. I hope you learned a lot from it.
If you want to master JavaScript, ES6+, React and Node.js with easy-to-understand content, do check out my YouTube channel and don't forget to subscribe.
Want to stay up to date with regular content regarding JavaScript, React, Node.js? Follow me on LinkedIn.