A beginner’s guide to JavaScript promises
Promises are considered ‘modern’ way to handle asynchronous code in JavaScript. Unlike callback functions, which are a way of adapting functions to handle tasks in sequence, promises are a functional feature of the language and custom-built to handle asynchronous code.
In this article we cover the fundamentals of promises: understanding, writing and using promises.
In case you are unfamiliar with asynchronous code in JavaScript, you may want to check out our earlier article on this topic before reading on.
What is a promise?
What happens when JavaScript encounters some asynchronous code in our script?
The JavaScript engines puts it to one side, deals with the rest of your code, and then returns to deal with the asynchronous code. Here is an example with setTimeout
:
setTimeout(function() { console.log("Executed"); }, 0); const x = "x"; console.log(x); // Console output: // x // "Executed"
You may expect "Executed"
to be logged before x
because the setTimeout
delay is set to 0 and is placed before console.log(x)
. But this isn’t the case. This is because (even if the time taken is 0 milliseconds) the JavaScript engine recognizes setTimeout
as asynchronous code, set it to one side, and returns to it after executing the rest of the script.
This is also true when we handle the result of a promise. However, because promises are designed to handle asynchronous code as efficiently as possible, they start running straight away (without blocking the rest of our script from executing).
The following example demonstrates this:
// Create a promise const myPromise = new Promise(function(resolve, reject) { console.log("promise running"); resolve("Fulfilled!") }); // Handle promise result myPromise .then(res => console.log(res)); // Synchronous code const x = "x"; console.log(x); // Console output: // "Promise running" // x // "Fulfilled!"
The first output is "promise running"
from inside the promise because it is executed straight away. Then "Fulfilled!"
only appears after x
despite being written before this in code.
Promises may remind you a little of functions: we create a promise and we handle the result of a promise. But unlike a function, it executes immediately (unless nested in a function) and the result is handled once the rest of our script has been executed and the process inside the promise is complete.
Writing a promise ✍
Much like we have to define a function before we can handle its result, a promise must first be created.
To write a new promise (and for working with promises in general) we use the Promise
object, which is available on the global window
object:
console.log(window.Promise); // ƒ Promise() { [native code] }
To create a promise, we create a new instance of the Promise
object by calling new Promise()
and pass in a function. This function has two parameters available to it: in the first position, resolve
, and the second, reject
(these are the standard names given to the parameters by convention):
new Promise(function(resolve, reject) { // Code to be executed } );
Now within the function statement, we write whatever action we would like to perform Now within the function statement, we can write whatever action we would like to perform (e.g. load a script or image).
Completing a promise
Promise-handling is initiated once the process we include inside the promise is complete.
But this does not happen automatically: we have to call resolve()
(if the process was complete successfully) or reject()
(if there was an error) when the process is complete.
Doing this has a similar effect to calling the keyword inside of a function statement in that calling resolve()
or reject()
execution ends and whatever is passed inside of resolve()
or reject()
is what the promise returns.
It is useful that we have two options for completing the promise because this passes information on to the handling of the result about whether it completed successfully. In the terminology of promises, a successful promise is known as a ‘fulfilled’ promise:
For example:
new Promise(function(resolve, reject) { resolve("Fulfilled!") });
And an unsuccessful promise is a ‘rejected’ promise:
new Promise(function(resolve, reject) { reject("Rejected!") });
It is important to know that once a promise is either fulfilled or rejected, it cannot change status. If both resolve and reject are called, it is the first one that the JavaScript engine encounters that will determine the promise’s result:
For example, the following promise will rejected because reject
is encountered before resolve
:
new Promise(function(resolve, reject) { reject("Rejected!"); resolve("Fulfilled!" });
Using a promise 🔑
To use or ‘consume’ a promise (in promises terminology) requires a special syntax. We again have to call on the Promise object.
If we try to log the contents of the promise, it returns the following:
const myPromise1 = new Promise(resolve => setTimeout(() => {return resolve("I am the return value")}, 1000)) console.log(myPromise1); // Promise {<pending>}
The promise is ‘pending’ because we have tried to log its contents before it is complete.
But even if we try to log its contents at a later point in time, all that is returned is it status. If its has resolved successfully, the status would read ‘fulfilled’ and ‘rejected’ if there is an error.
This information gives us some important information about the promise but are only reading a status message.
.then() and .catch()
The access the return value of a promise we can use the .then method, which is available on a Promise
object.
Within the .then method, we handle the result of the promise with a function we place in the first argument position. As a parameter, we have available the return value of the promise (commonly named res
).
const myPromise1 = new Promise(resolve => setTimeout(() => {return resolve("I am the return value")}, 1000)) myPromise1 .then(res => console.log(res)) // Output: "I am the return value" (after 1 sec)
Now a promise is either fulfilled or rejected. The above deals with a successful result. But what if the promise is rejected?
One way to handle the error is to write another function in the second argument position within .then
. As a parameter, the error return value is available:
const myPromise1 = new Promise((resolve,reject) => setTimeout(() => {return reject("This is the error return value") return resolve("I am the return value") }, 1000)) myPromise1 .then(res => console.log(res), err => console.log(err)) // Output: "This is the error return value" (after 1 sec)
While it is good to know that an error can be handled with a function placed in the second position of .then
, this is not the most common way to handle a rejected promise. Instead, the .catch
method is usually used after .then
.
Placing .catch
after .then
in the example above and handling the error with a function inside it will have exactly the same outcome:
const myPromise1 = new Promise((resolve,reject) => setTimeout(() => {return reject("This is the error return value") return resolve("I am the return value") }, 1000)) myPromise1 .then(res => console.log(res)) .catch(err => console.log(err)) // Output: "This is the error return value" (after 1 sec)
In this example, the rejected promise immediately triggers the .catch
statement to be executed. The first argument function inside .then
is ignored.
In this example, the use of .catch
or a function is the second argument position of .then
for handing the result of the promise is a matter of preference. But .catch
is a real syntax-saver when chaining .then methods, as we will see below.
.then chaining
The result of a promise can be passed down a chain of .then
methods.
For a result to be passed from one .then
to the next, the first .then
function must provide a return value. This is then available in the next .then
function as a parameter.
This process can be repeated as many times as you need:
const myPromise1 = new Promise((resolve,reject) => setTimeout(() => {return resolve("I am the return value") return reject("This is the error return value") }, 1000)) myPromise1 .then(res => res) .then(res => res+"!") .then(res => res+"!") .then(res => res+"!") .then(res => console.log(res)) .catch(err => console.log(err)) // Output: I am the return value!!!
And here is the value of the .catch
statement in this content: if there is an error passing down the result through any of the .then
statements, a message with information about the error will be immediately sent to the .catch
statement. No further .then
statements will be executed.
const myPromise1 = new Promise((resolve,reject) => setTimeout(() => {return resolve("I am the return value") return reject("This is the error return value") }, 1000)) myPromise1 .then(res => res) .then(res => res+"!") .then(res => JSON.parse(res)) .then(res => res+"!") .then(res => console.log(res)) .catch(err => console.log(err)) // SyntaxError: Unexpected token I in JSON at position 0
So with .then
chaining, .catch
avoids having the handle each potential .then
error. It will ‘catch’ an error anywhere in the chain and handle it in the way we specify.
Sequencing three tasks with promises 1️⃣→2️⃣→3️⃣
Now for a more practical example. Let’s schedule three tasks to finish in a certain order.
Here are the three tasks to be scheduled using promises that are currently completing in the incorrect order:
setTimeout(function(){ console.log("First task") },700); setTimeout(function(){ console.log("Second task") },1500); setTimeout(function(){ console.log("Third task") },500); // Output: // "Third task" // "First task" // "Second task"
To order these using promises, we first write three new promises, placing each task within them. And we call resolve
with the return value passed in. This means that each of the promises will change status from ‘pending’ to ‘fulfilled’ once its timeout is complete:
const myPromise1 = new Promise(function(resolve, reject) { setTimeout(function(){ resolve("First task"); },700); }); const myPromise2 = new Promise(function(resolve, reject) { setTimeout(function(){ resolve("Second task"); },1500); }); const myPromise3 = new Promise(function(resolve, reject) { setTimeout(function(){ resolve("Third task"); },500); });
We then do something we haven’t covered yet: store references to each promise within an array in the order we want them to complete:
const myPromises = [myPromise1, myPromise2, myPromise3];
Finally, we handle the result of each promise.
Because there are multiple promises, we have to handle the result slightly differently from above. We have to use the .a
ll method available on the Promise
object constructor and pass in the array of promises.
The handling is otherwise the same as for a single promise using .then
and .catch
:
const myPromise1 = new Promise(function(resolve, reject) { setTimeout(function(){ resolve("First task"); },700); }); const myPromise2 = new Promise(function(resolve, reject) { setTimeout(function(){ resolve("Second task"); },1500); }); const myPromise3 = new Promise(function(resolve, reject) { setTimeout(function(){ resolve("Third task"); },500); }); const myPromises = [myPromise1, myPromise2, myPromise3]; Promise.all(myPromises) .then(res => console.log(res)) .catch(err => console.log(err)) // Output: ['First task', 'Second task', 'Third task'] (after 1.5 seconds)
Notice something different about this outcome from the callbacks method?
With callbacks, the result is processed serially. For the above example, this would mean “First task” is returned after 0.7 seconds, then “Second task” would be returned 1.5 seconds after task 1 is complete and “Third task” 0.5 seconds after task 2 is complete.
But using Promise.all
the promises run in parallel and the results are returned in order once all are complete.
Serial promise execution
But what is we want to execute the tasks serially? For example, we may want to show our users something as soon as the first task is complete.
For this we use an async for...await
loop.
As before, we place our promises in an array. But this time, instead of Promise.all
, we create an asynchronous function with a loop inside that ‘awaits’ each promise before executing the next one:
// 1) Create an array of promises const myPromises = [ myPromise1 => new Promise(resolve => setTimeout(() => {return resolve("One person")}, 1000)), myPromise2 => new Promise(resolve => setTimeout(() => {return resolve("Two people")}, 1000)), myPromise3 => new Promise(resolve => setTimeout(() => {return resolve("A crowd 🎉")}, 1000)), ]; // 2) Create an async for...await loop in an async function async function executeMyPromises(myPromises) { for (let promise of myPromises) { console.log(await promise()); } }; // 3) Run async function executeMyPromises(myPromises); // "One person", "Two people", "A crowd 🎉"
Other ways to handle multiple promises
As you can see, the results of promises can be handled in parallel and serially, making them more flexible than callbacks (serial only).
But it doesn’t stop there. There are more methods available on the Promise constructor object to handle multiple promises.
Method: | Waits until: |
---|---|
Promise.all | All promises are resolved or a single one is rejected |
Promise.allSettled | All promises are either resolved or rejected |
Promise.any | A single promise resolves successfully |
Promise.race | A single promise resolves or rejects |
Promise-based HTTP requests with JavaScript’s Fetch API
In this tutorial, we have written promises and handled the results.
But sometimes we only need to handle the results of promises. JavaScript’s Fetch API, for example, returns a promise after making a HTTP request.
We therefore handle the result using .then
and .catch
syntax. Below is how the result of a GET request would be handled.
fetch('https://example-endpoint.com/api') .then(res => { // IF statement checks server response // .catch() does not do this! if (res.ok) { console.log ("Request successful"); return res } else { console.log("Request unsuccessful"); } }) .then(res => res.json()) .then(data => console.log(data)) // The data! .catch(error => console.log(error)) // Catch 'catches' any error with fetch itself
For more on using the Fetch API (including how to make a PUT, POST and DELETE request), check out our detailed previous post.
Summary
Promises are a modern and flexible way to handle asynchronous code in JavaScript.
In this tutorial we have covered what they are, how to write them, use them (singular and multiple) and a practical example of handling a promise returned from JavaScript’s Fetch API.
Related links:
- Mozilla MDN Web Docs: Promises
- NodeJS.dev: Understanding JavaScript Promises