How to Use JavaScript Promises Effectively
With the introduction of ES6, promises became available in JavaScript. Before promises, callbacks provided a way to handle asynchronous operations. They were a powerful means of executing code once an asynchronous operation had finished. However, JavaScript has evolved, and now the use of promises is the preferred method for handling asynchronous operations.
In this tutorial, we'll look into the Promise API in JavaScript. But first, to avoid any confusion, letβs define what exactly callbacks are.
What are Callbacks
Callbacks are functions passed as arguments to other functions to be used once an event has occurred or a task is finished. The simplest example of a callback can be an event listener for a click event:
- index.js
- index.html
The second argument we pass to addEventListener
is a callback. When this function is attached to an HTML element in the document, it'll only run once a user clicks on the element. Using callbacks for such asynchronous operations is a perfect solution. However, callbacks were also used for handling the results of network requests, resulting in code often referred to as "callback hell."
Callback hell occurs when we have to wait for multiple consecutive events that depend on the result of each other. Imagine loading a list of images in JavaScript to later operate on the image data. We start wrapping one callback into another until we end up with code that looks like a Christmas tree:
getImage('base', data0 => {
getImage(data0, data1 => {
getImage(data1, data2 => {
getImage(data2, data3 => {
getImage(data3, data4 => {
getImage(data4, data5 => {
getImage(data5, data6 => {
getImage(data6, data7 => {
console.log('All images have been retrieved π')
})
})
})
})
})
})
})
})
What are Promises
So how can we get around nesting callbacks? Of course, the alternative, which this article is about, is the use of promises. The Promise object is a Web API accessible through the global window
object. Itβs an asynchronous operation that can be used in place of a callback. The Promise
object can always be in one of three states.
Initially, it has a pending state, meaning the promise hasnβt finished its operation yet. When the promise is completed, it enters one of two states:
- Fulfilled: meaning that the operation was completed successfully.
- Rejected: meaning that the operation failed due to an error.
Advantages
There are several advantages to using a promise over a callback; however, don't forget that there are some drawbacks as well. Some of the advantages of using a promise over a callback include:
- They are composable, unlike callbacks, therefore, we can avoid callback hell.
- We can easily execute code with
Promise.all
when multiple responses are returned. - We can wait for only one result from concurrent pending promises with the help of
Promise.race
. - We can synchronously write asynchronous code if we use them in conjunction with the
async
/await
keywords. - Promises support chaining, meaning we can compose sequences of asynchronous operations more easily. This chaining allows for more concise and expressive code compared to nested callbacks.
Disadvantages
Of course, everything comes at a price. Just like everything else, promises also have disadvantages. While they're a perfect choice over callbacks and have many advantages, there are some minor disadvantages we need to cover as well. Some of the disadvantages include:
- They can only operate on a single value at a time.
- They are slower than using callbacks, which can cause performance issues if used incorrectly.
- They can increase code complexity, especially when dealing with multiple asynchronous operations or error handling.
Yet, because of their composability, they're a better choice over callbacks. So how can we use them in our code? Letβs see a couple of examples.
How to Use Promises
To create a new promise in JavaScript, we can use the new
keyword combined with the Promise
object:
new Promise((resolve, reject) => { ... })
Each promise accepts a callback function with two parameters:
resolve
: A function for resolving the promise. This makes the promise fulfilled, meaning the operation is completed successfully.reject
: A function for rejecting the promise. When a promise is rejected, it means there was an error.
We can call these functions inside the callback of the promise to either resolve or reject a promise:
// βοΈ Resolves promise with a value
new Promise((resolve, reject) => {
resolve(value)
})
// β Rejects promise with a reason
new Promise((resolve, reject) => {
reject(reason)
})
To use the resolved or rejected value, we can chain a then
callback from the created promise. Take the following as an example:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('I will be resolved after 1 second')
}, 1000)
})
// The value of `result` will be the value passed into the `resolve` function
promise.then(result => console.log(result))
The above example will resolve the promise after 1 second with a text. To use the resolved value, we can store the promise in a variable and call the then
method on the variable. The then
method accepts a callback function. Inside the callback, we have access to the resolved/rejected value.
A then
callback is called whenever a promise finishes execution. This can be chained as many times as necessary, as long as we also return another promise from the then
callback:
getUser(user)
.then(getUserSettings)
.then(getUserMetrics)
.then(response => console.log(response))
In this example, once the user is resolved, we then execute the getUserSettings
function. Imagine that this function is dependent on the resolved user. This way, we can wait for the result of the user to come back before getting the settings associated with it. Then, once the settings are also available, we get the metrics with getUserMetrics
. Finally, we can return with all the necessary responses.
Note that this is only an example to demonstrate a promise. In an ideal case, you would get all this information in just one go.
We can also handle errors with a catch
clause, or execute code after the promise is settled by either a fulfill or reject with finally
:
const promise = new Promise(...)
promise.then(result => console.log(result))
.catch(error => console.log(error))
.finally(info => console.log(info))
Promise.all
Now let's consider handling multiple promises. For example, suppose we want to retrieve multiple users at once. We can use Promise.all
and call a single callback in the following way:
- Lines 1-9: First, we define a mock function that returns a fake promise, either resolved or rejected based on the
value
parameter. If thevalue
istrue
, the promise is resolved; otherwise, it'll be rejected. This function is only for demonstration purposes. - Lines 11-13: Using the
fakePromise
function, we create three different promises, all with a truthy value, meaning all promises will be resolved. - Lines 15-18:
Promise.all
expects an array of promises. Thethen
handler will be called whenever all three promises have been fulfilled.
Try changing one of the promises to return false
. As soon as one promise is rejected from the many, Promise.all
will short-circuit and execute the catch
block.
Promise.any
What if we're only interested in one of many promises that resolve the fastest? In that case, we can use Promise.any
:
const promise1 = fakePromise(true)
const promise2 = fakePromise(1)
const promise3 = fakePromise('resolved')
Promise.any([promise1, promise2, promise3]).then(result => {
// result will be the fastest fulfilled Promise
console.log(result)
}).catch(error => console.log(error))
This is different from Promise.all
as it'll only return one promise, the one that resolves the fastest. In this case, it'll return true
as that is the first promise we created.
Try copying the above code into the interactive editor, then change the argument for the first function call from true
to false
. It'll return 1. This is because the first promise will be rejected, so it'll always return the fastest resolving promise.
Promise.race
Now, what if we're only interested in the fastest promise, but we're also interested in rejected promises? For those cases, we have Promise.race
. Just like Promise.any
, this also returns a single promise from many. But unlike Promise.any
, it'll also return the promise if it's rejected.
const promise1 = fakePromise(false)
const promise2 = fakePromise(1)
const promise3 = fakePromise('resolved')
Promise.race([promise1, promise2, promise3]).then(result => {
// Result will be the fastest fulfilled Promise.
// If it fails, it will move to the catch clause
console.log(result)
}).catch(error => console.log(error))
In this example, the first promise will be rejected due to the false
value. But now we donβt get back 1 as we did for Promise.any
, but we get back the false
value because Promise.race
returns the promise whether it's fulfilled or rejected.
Further Enhancing Readability
To make things even simpler, we can use the async
/await
keywords in conjunction with promises to write asynchronous code in a synchronous manner. In line with the previous examples, instead of Promise.all
, we could write:
(async = () => {
const result1 = await promise1
const result2 = await promise2
const result3 = await promise3
// This console.log will only be called once all promises have been returned
console.log(result1, result2, result3)
})()
Here, every variable returns a promise. By using the keyword await
in front of them, we tell JavaScript to wait for the completion of the promise. Note that to use the await
keyword, we must be inside an async
function. If we don't want to create a function just for using the await keyword, we can use an IIFE instead.
Summary
Now that you understand everything about promises and have it in your toolkit, make sure you never fall into the trap of callback hell again. Do you have any questions? Leave a comment! If you'd like to learn more about JavaScript, be sure to check out the following roadmap:
Rocket Launch Your Career
Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies: