DEV Community

Cover image for JavaScript Promises: Everything You Need to Know
Aravind Sanjeev
Aravind Sanjeev

Posted on • Updated on • Originally published at humaneer.org

JavaScript Promises: Everything You Need to Know

In today's post, I promise to give you the best article on JavaScript promises you ever read. See what I did there? A promise in JavaScript is an object that is returned after an asynchronous operation. The object will be returned regardless of whether the asynchronous operation was a success or failure. This is why it is called a promise.

The main purpose of using promise is to avoid callback hell. In this post, we will learn about JavaScript promises, what they are, what they can do, and how to use them.

To start off, we will discuss what existed before promise was introduced. Before promise, we used to pass callback functions directly as an argument.

Let's take the example of an asynchronous function that downloads a certain image.

DownloadImageAsync()
Enter fullscreen mode Exit fullscreen mode

The above function has to accept three arguments,

  • The url of the image
  • A function that is executed after the image is successfully downloaded
  • A function that is executed after the image download failed

The two functions are called callback functions. Without callback functions, we cannot act up on the result of an asynchronous operation.

function success() {
  console.log("success")
}

function failure() {
  console.log("failure")
}

DownloadImageAsync(url, success, failure)
Enter fullscreen mode Exit fullscreen mode

This is how a typical asynchronous operation is structured. The problem with this approach start when we try to run several asynchronous operations.

asyncFunctionOne(function (result) {
  asyncFunctionTwo(
    result,
    function (newResult) {
      asyncFunctionThree(
        result,
        function (lastResult) {
          console.log(lastResult)
        },
        failure
      )
    },
    failure
  )
}, failure)
Enter fullscreen mode Exit fullscreen mode

What you see above is called a callback hell. This will get worse as more asynchronous operations are added. This is where JavaScript promises sweeps in with a better syntax.

As I already mentioned, a promise in JavaScript is an object that is returned after an asynchronous operation. We will get to creating promises later down the post. For now, let's rewrite the above code for promises.

asyncFunctionOne()
  .then(result => asyncFunctionTwo(result))
  .then(newResult => asyncFunctionThree(newResult))
  .then(lastResult => console.log(lastResult))
  .catch(failure)
Enter fullscreen mode Exit fullscreen mode

Each asynchronous functions above returns a promise. In a promise, we can attach the then method where we pass the next function. From the above code you should take away three things,

  • Promises are far shorter and simpler in syntax
  • You only need to pass the failure callback once
  • A promise must always return results (check arrow function syntax, it is returning results which is passed to the next then block)

It is important to note that for most of our JavaScript life, we will be using already-made promises instead of creating them ourselves. With that being said, let's learn to create promises.

Creating a promise

To create promises, we use the Promise constructor object in JavaScript. The new keyword is used to create a new instance of a user-defined object or constructor.

const promise = new Promise()
Enter fullscreen mode Exit fullscreen mode

A promise has to be resolved (success) or rejected (failure). In both cases, a function is executed (as demonstrated above). We pass these functions as arguments of an arrow function passed to the Promise constructor.

const promise = new Promise((resolve, reject) => {
  //
})
Enter fullscreen mode Exit fullscreen mode

Inside the arrow function, we create the definition and condition for resolve or reject to be executed.

const promise = new Promise((resolve, reject) => {
  const num = 3
  if (1 + 2 == num) {
    resolve("success")
  } else {
    reject("failed")
  }
})
Enter fullscreen mode Exit fullscreen mode

We already saw how to consume the promise. By using then and catch methods.

promise
  .then(msg => console.log("from then " + msg))
  .catch(msg => console.log("from catch " + msg))

// output: from then success
Enter fullscreen mode Exit fullscreen mode

If the condition is satisfied (which it is), the resolve function is triggered and then method is executed. Otherwise, the catch method is executed.

The then method above itself returns another promise.

const promise2 = promise
  .then(msg => console.log("from then " + msg))
  .catch(msg => console.log("from catch " + msg))
Enter fullscreen mode Exit fullscreen mode

We can use then method again on the new promise object.

promise2.then(something => something)
Enter fullscreen mode Exit fullscreen mode

Better way to do this is to just tuck in the then methods one after another (as we saw in the first example).

const promise2 = promise
  .then(msg => console.log("from then " + msg))
  .then(something => something)
  .catch(msg => console.log("from catch " + msg))
Enter fullscreen mode Exit fullscreen mode

This is called a promise chain.

To recap,

  • A Promise is an object (constructor) in JavaScript
  • An instance of the object is created using new keyword
  • A function is passed as an argument to the constructor
  • The function accepts two other functions as arguments - resolve & reject
  • Conditions and definitions for these functions are defined in the function definition
  • The then and catch methods are executed for resolve and reject accordingly
  • The then method itself returns a promise that can attach another then method creating a promise chain

States of a promise

A promise always has to be in one of the following three states:

  • Pending
  • Fullfilled
  • Rejected

Pending is the initial state of the the promise. The promise is fullfilled when it is a success. The promise is rejected when it is a failure.

When a promise is rejected, it is still returning another promise. That means we can also use then method instead of catch.

const promise = new Promise()

promise
  .then(onSuccess => console.log(onSuccess))
  .then(onRejection => console.log(onRejection))
Enter fullscreen mode Exit fullscreen mode

But then method in this case is used to execute another asynchronous operation. It should not be confused with catch which is used for error handling. A catch is always necessary.

const promise = new Promise()

promise
  .then(onSuccess => console.log(onSuccess))
  .then(onRejection => console.log(onRejection))
  .catch(error => console.log(error))
Enter fullscreen mode Exit fullscreen mode

The catch method is instantly executed when confronted with an error. It doesn't matter how many then methods it had to skip.

The catch method also returns a promise. So we can tuck in a then method after the catch method.

const promise = new Promise()

promise
  .then(onSuccess => console.log(onSuccess))
  .then(onRejection => console.log(onRejection))
  .catch(error => console.log(error))
  .then(somethingElse => somethingElse)
Enter fullscreen mode Exit fullscreen mode

This way we can still trigger another asynchronous operation after error handling.

If you want to execute something regardless of whether your promise is fulfilled or rejected, use finally.

const promise = new Promise()

promise
  .then(msg => console.log("from then " + msg))
  .then(onRejection => console.log(onRejection))
  .catch(msg => console.log("from then " + msg))
  .then(somethingElse => somethingElse)
  .finally(console.log("whatever"))
Enter fullscreen mode Exit fullscreen mode

Whatever goes in to the finally block is always executed.

The resolve() and reject() methods

When we created a promise earlier, we defined the condition for resolving or rejecting that promise. But there is a way to resolve or reject a promise unconditionally.

Promise.resolve() method returns a promise object that is already resolved by a given value.

Promise.resolve(value)
Enter fullscreen mode Exit fullscreen mode

Promise.reject() method returns a promise object that is rejected with a given reason.

Promise.reject(reason)
Enter fullscreen mode Exit fullscreen mode

Creating promises that are unconditionally resolved or rejected is particularly useful when handling multiple independent promises.

Handling multiple independent promises

We can handle multiple independent promises using Promise.all() and Promise.race() methods. Both methods accept an array of promises.

While using Promise.all(), the then method is only triggered after the last promise is resovled.

const promise1 = Promise.resolve("result1")
const promise2 = Promise.resolve("result2")
const promise3 = Promise.resolve("result3")

Promise.all([promise1, promise1, promise3]).then(results =>
  console.log(results)
)

// [ result1, result1, result3 ]
Enter fullscreen mode Exit fullscreen mode

While using Promise.race(), the then method is triggered the moment first promise is resolved.

const promise1 = Promise.resolve("result1")
const promise2 = Promise.resolve("result2")
const promise3 = Promise.resolve("result3")

Promise.race([promise1, promise1, promise3]).then(results =>
  console.log(results)
)

// result1
Enter fullscreen mode Exit fullscreen mode

Since these are simple promises, promise1 will be resolved first. This may not be the case with complex ones. This enables us to run multiple asynchronous operations and act up on the one that finishes first.

Discussion (0)