ELI5: Promises in JavaScript
Paul Akinyemi
Posted on January 30, 2022
Intro
Promises are an integral part of asynchronous programming in JavaScript. If you need to do any asynchronous work, chances are you'll need to work with them. But how exactly do Promises work, and how can we use them in our code?
This article explains the basics of Promises in JavaScript, but it does not cover async/await. After you finish reading, you should:
posses an intuitive understanding of how Promises work
understand how to create and use Promises
Prerequisites
This article assumes the reader understands the following concepts:
If you don't already understand those, visit the links above!
What are JavaScript Promises?
A Promise is a JavaScript object that makes it easy for us to write asynchronous code. You can think of a promise as a sort of code IOU. A Promise serves as a placeholder for a value that isn't available yet, and it provides the requested value when said value is available.
JavaScript Promises work much like the non-code kind. When someone makes you a promise, they're saying: I can't do this for you yet, but I'll try my best, then get back to you.
JavaScript Promises are similar. A piece of code requests a resource that isn't available, the same way you might ask for a gift from a friend. In response, the requesting code gets a particular object: a Promise.
This object allows the providing code to deliver the resource when it's ready or notify the requesting code of its failure, the way your friend might come to you later to deliver your gift.
Here's another definition from MDN:
A Promise is an object representing the eventual completion or failure of an asynchronous operation.
I prefer a slightly different phrasing:
A Promise represents an asynchronous operation that will eventually run to completion or encounter an error in the process.
The State of a JavaScript Promise
A Promise can exist in one of three states:
- The pending state, where all Promises start.
- The fulfilled state, which means the operation is complete.
- The rejected state, which means the operation failed.
A Promise exists in the pending state when the operation it represents hasn't run to completion.
A Promise moves to the fulfilled state if the operation it represents runs successfully.
If the operation fails, the Promise moves to the rejected state.
When a Promise moves to either the fulfilled or rejected state, we say that the Promise has "settled".
Creating a Promise
The syntax for creating a Promise is new Promise(function)
.
The function we pass to the Promise is where the code that will request the desired resource lives. That function has two mandatory arguments: resolve() and reject().
Both arguments are functions that the browser will supply.
We call resolve() in our function when our asynchronous code executes successfully, and we call reject() if we can't complete the operation.
We call the value we pass to resolve() the "fulfilment value", and the value we pass to reject() the "rejection reason".
Here's an example of creating a Promise:
const Promise = new Promise((resolve, reject) => {
// do some async stuff
// if code is successful
resolve(value)
// we couldn't complete the operation for some reason
reject(reason)
})
Using Promises
We can use Promises in two ways:
- async/await
- Promise instance methods
We won't be covering async/await in this article, but you can read about it here.
Using Promise instance methods
Remember that a Promise is a kind of IOU for the result of an operation? We use Promises by passing the code we want to use the result of that operation (the code that claims the IOU) to one of three instance methods:
- then() method
- catch() method
- finally() method
All instance methods only run after the Promise they belong to have settled, and all instance methods return a new Promise.
The then() method
The then() method accepts up to two functions as arguments.
The first argument contains the code you want to run if the Promise fulfils, and the second contains code that should run if the Promise rejects.
Both arguments to then() are optional. If we don't provide a callback to a then() method corresponding to the parent Promise's current state, the method will return a new Promise in the same state as its parent Promise.
Here's an example:
// we can use then() like this:
demoPromise.then(successCallback, failureCallback)
// or if we don't care about failure:
demoPromise.then(successCallback)
// if demoPromise is in the rejected state,
// the above lcode will immediately return a new rejected Promise
// we can handle only failure like this:
demoPromise.then(undefined, failureCallback)
// if demoPromise is in the fulfilled state,
// this line will immediately return a new fulfilled Promise
// not very useful, but it won't cause an error
demoPromise.then()
The catch() method
The catch() method receives one mandatory argument.
The purpose of the catch() method is to handle the failure of the operation the Promise represents. The argument to catch() contains the code that we want to run if the operation fails.
The calling the catch() method works the same as calling then(undefined, failureCallback)
.
The function passed to catch receives the rejection reason of the parent Promise as its argument.
The finally() method
The finally() method receives a single function as its argument. The argument to finally() contains code that we want to execute regardless of the success or failure of the operation the Promise represents, and the function passed to finally() never receives an argument.
So now we can use the value represented by a single Promise, but what do we do when we want to execute multiple operations back to back, because the second operation depends on the first? We use Promise Chaining.
Promise Chaining
Promise chaining is a technique where you attach one instance method to another to execute successive operations. Promise chaining is possible because each instance method returns a new settled Promise, which becomes the parent of the following instance method in the chain.
Let's create an example:
const demoPromise = fetch("https://example.com/resource.json")
demoPromise.then((response) => {
// do some cool stuff
return value 1
}).then((response) => {
// first then() returns a new, already settled Promise
// value 1 is the fulfillment value that this then() receives
// we can now do something with value 1
someOperation(value1)
}).catch((err) => {
//handle error if something goes wrong in producing value 1
})
Chained Promise methods usually execute one after another, except when an operation in the chain fails and throws an error.
If this happens, the method that raised the error returns a rejected Promise. The next method to execute is the closest method that has a failure callback (a then() with two arguments or a catch() method).
Execution resumes from the then() method after the method that handled the error, if there is any.
Here's an example of a Promise chain:
const demoPromise = fetch("https://example.com/promise.json")
demoPromise.then((response) => {
// an error occurs
}).then((response) => {
// this function won't run
}).catch((err) => {
//handle error
}).then((err) => {
//resume execution after the error
}).catch((err) => {
// handle any new errors
})
This section covered how to execute successive asynchronous operations, but what if the code we need to run depends on the result of multiple Promises at once?
Using Multiple Promises Together
So what do you do when you want to run code that depends on two or more Promises that need to run simultaneously? We use the static methods of the Promise Class.
The Promise class has six static methods in total, but we'll only talk about the three you're most likely to need:
- Promise.all()
- Promise.race()
- Promise.any()
All static methods take multiple Promises as their argument and return a single Promise based on the settled states of the argument Promises.
Promise.all()
Promise.all() allows you to attach an instance method to a Promise whose fulfilment value is an array of the fulfilment values of the Promises passed to Promise.all().
The Promise the instance method is attached to only moves to the fulfilled state when all Promises passed to Promise.all() have moved to the fulfilled state.
Once this happens, Promise.all() returns a new fulfilled Promise to the instance method.
If any of the input Promises rejects, Promise.all() returns a settled Promise in the rejection state, whose rejection reason is the reason of the first Promise to reject. Any Promises still in the pending state are ignored, regardless of which state they settle into.
Let's look at an example:
const multiPromise = Promise.all(fetch('resource1.json'), fetch('resource2.json'), fetch('resorce3.json'))
multiPromise.then((arrayOfFulfilledValues) => {
// process all the fulfilled values
return value 1
}).catch((err) => {
// process the first rejection that happens
})
Promise.race()
Promise.race() is similar to Promise.all().The difference is: the Promise returned by Promise.race is simply the first Promise to settle. Once any Promise moves to the fulfilled or rejected states, Promise.race() ignores the other input Promises.
const firstSettledPromise = Promise.race(fetch('resource1.json'), fetch('resource2.json'), fetch('resorce3.json'))
firstSettledPromise.then((firstResolvedValue) => {
// process the first fulfilled value
return value 1
}).catch((err) => {
// process the first rejection that happens
// whether in the Promise race or in the then()
})
Promise.any()
Promise.any() is like Promise.race(), but it will wait for the first Promise to move to the fulfilled state, instead of the first Promise to settle.
If an input Promise moves to the rejected state, Promise.any() does nothing as long as other Promises are still in the pending state.
If all the input Promises reject, Promise.any() returns a rejected Promise with an Aggregate Error, containing all the rejection reasons.
const firstFulfilledPromise = Promise.any( fetch('resource1.json'),
fetch('resource2.json'), fetch('resorce3.json') )
firstFulfilledPromise.then((firstResolvedValue) => {
// process the resolved value
return value 1
}).catch((err) => {
// process the Aggregate error or
// an error that occurs in the then()
})
Use cases of Promises
Typically, working with Promises in the real world involves consuming Promises returned to you from a browser API or JavaScript method.
It's relatively rare that you'll have to create a Promise in your code. Here are some of the most common APIs and functions that return Promises:
- The Fetch API
- Response.json()
Conclusion
In this article, we covered the basics necessary to work with Promises. If you would like to learn more, visit:
Posted on January 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.