How to Use JavaScript Promises Effectively

How to Use JavaScript Promises Effectively

Putting a stop to callback hell
Ferenc Almasi β€’ Last updated 2024 March 05 β€’ Read time 10 min read
Learn how asynchronous code works in JavaScript, and how we can use promises in place of callbacks to achieve asynchronicity.
  • twitter
  • facebook
JavaScript

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.js
  • index.html index.html
Refresh

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:

Copied to clipboard! Playground
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 πŸŽ‰')
                            })
                        })
                    })
                })
            })
        })
    })
})
Callback hell happens where multiple callbacks are nested into each other

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:

  • check
    They are composable, unlike callbacks, therefore, we can avoid callback hell.
  • check
    We can easily execute code with Promise.all when multiple responses are returned.
  • check
    We can wait for only one result from concurrent pending promises with the help of Promise.race.
  • check
    We can synchronously write asynchronous code if we use them in conjunction with the async/await keywords.
  • check
    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:

  • close
    They can only operate on a single value at a time.
  • close
    They are slower than using callbacks, which can cause performance issues if used incorrectly.
  • close
    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.

Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScriptinfo Remove ads

How to Use Promises

To create a new promise in JavaScript, we can use the new keyword combined with the Promise object:

Copied to clipboard!
new Promise((resolve, reject) => { ... })
How to create a promise in JavaScript

Each promise accepts a callback function with two parameters:

  1. resolve: A function for resolving the promise. This makes the promise fulfilled, meaning the operation is completed successfully.
  2. 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:

Copied to clipboard! Playground
// βœ”οΈ Resolves promise with a value
new Promise((resolve, reject) => {
    resolve(value)
})

// ❌ Rejects promise with a reason
new Promise((resolve, reject) => {
    reject(reason)
})
How to resolve or reject a promise in JavaScript

To use the resolved or rejected value, we can chain a then callback from the created promise. Take the following as an example:

Copied to clipboard! Playground
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))
How to use resolved or rejected values

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:

Copied to clipboard!
getUser(user)
    .then(getUserSettings)
    .then(getUserMetrics)
    .then(response => console.log(response))
How to chain multiple then callbacks

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:

Copied to clipboard! Playground
const promise = new Promise(...)

promise.then(result => console.log(result))
       .catch(error => console.log(error))
       .finally(info => console.log(info))
How to handle errors with promises

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:

🚫 Refresh console
  • Lines 1-9: First, we define a mock function that returns a fake promise, either resolved or rejected based on the value parameter. If the value is true, 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. The then 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:

Copied to clipboard! Playground
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))
Use Promise.any if you need to wait for only the fastest Promise to be fulfilled

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.

Copied to clipboard! Playground
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))
Use Promise.race if you need to wait for only the fastest Promise that either fulfills or rejects

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:

Copied to clipboard! Playground
(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)
})()
async.js

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:

JavaScript Roadmap
  • twitter
  • facebook
JavaScript
Did you find this page helpful?
πŸ“š More Webtips
Mentoring

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:

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.