JavaScript Promises: An Introduction

By Hemanta Sundaray on 2023-02-07

What is a promise?

A promise is an object that represents the eventual completion or failure of an asynchronous operation.

An asynchronous operation is simply an operation that is executed in the background, allowing the program to continue executing. It is often used to perform long-running tasks, such as loading data from a remote server, without blocking the rest of the program.

For example, consider the following code sample:

console.log("Start")

setTimeout(() => {
  console.log("Timeout complete")
}, 2000)

console.log("End")

This code defines a setTimeout function, which is an asynchronous operation that waits for a specified amount of time (2 seconds) before executing a callback function. The code logs the strings "Start" and "End" before and after the setTimeout function, respectively.

When you run this code, you will see the following output:

Start
End
Timeout complete

As you can see, the console.log("End") statement is executed immediately after the console.log("Start") statement, even though the setTimeout function is still waiting for its specified amount of time to elapse. This demonstrates how an asynchronous operation can run in the background without blocking the rest of the program.

The key to understanding how non-blocking works is to understand that the JavaScript engine is single-threaded, which means that it can only execute one task at a time. However, asynchronous operations allow the JavaScript engine to continue executing other tasks while it waits for the asynchronous operation to complete. This is why the rest of the program is not blocked while the asynchronous operation is running.

Promise terminology

A promise can be in 3 possible states:

JavaScript promises

  • Pending: The promise is pending (not yet settled).
  • Fulfilled: the promise has been settled with a value. This usually means success.
  • Rejected: The promise has been settled with a rejection reason. This usually means failure.

Promise methods

A JavaScript promise has 3 methods for registering handlers: then, catch, and finally.

then

The then method adds a fulfillment handler to call if/when the promise gets fulfilled. The resolved value gets passed to the callback function registered as the first argument of then().

Consider the following:

p2 = p1.then(callback 1);

Above, the then() method registers a fulfillment handler, creating and returning a new promise (p2). The new promise (p2) will get fulfilled or rejected depending on what happens to the original promise (p1) and what you do in your handler (callback 1). If p1 gets rejected, the handler isn’t called and p2 is rejected with p1’s rejection reason. If p1 gets fulfilled, the handler gets called.

catch

The catch() method adds a rejection handler to call if/when the promise gets rejected. The rejected value (typically an Error object) gets passed to the callback function registered with catch().

p2 = p1.catch(error => doSomethingWith(error))

Above, catch() registers a rejection handler on p1, creating and returning a new promise (p2). The new promise (p2) will get fulfilled or rejected depending on what happens to the original promise ( p1) and what you do in your handler. If p1 gets fulfilled, the handler isn’t called and p2 is fulfilled with p1’s fulfillment value. If p1 gets rejected, the handler gets called.

finally

finally adds a finally handler to call if/when the promise gets settled (fulfilled or rejected). It is very much like the finally block of a try/catch/finally. It adds a handler that gets called whether the promise gets fulfilled or rejected (like a finally block).

Like then and catch, it returns a new promise that gets fulfilled or rejected based on what happens to the original promise and what the finally handler does. But unlike then and catch, its handler is always called.

Note: One of the key aspects of promises is that then, catch and finally return a new promise. The new promise is connected to the promise you called the method on - it will get fulfilled or rejected based on what happens to the original promise and what your handler function does.

Creating promises

Most of the time, you will be consuming promises (for example, fetching data from an API endpoint that returns a promise). If you need to create a promise, when you don’t have one, you can use the Promise constructor.

The Promise constructor

The function you pass into the Promise constructor is called an executor function. It is responsible for starting the process for which the promise will report success or failure.

The executor function receives two arguments.

The first argument is the function (typically given the name resolve) that is called to resolve the promise. The second argument (typically given the name reject) is the function that is called to reject the promise.

Note that nothing can settle the promise unless it has access to either of these functions (resolve or reject).

Example:

const example = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        const succeed = Math.random() < 0.5
        if (succeed) {
          console.log("resolving with 24 (will fulfill the promise)")
          resolve(24)
        } else {
          console.log("rejecting with new Error ('failed')")
          throw new Error("failed")
        }
      } catch (error) {
        reject(error)
      }
    }, 100)
  })
}

example()
  .then(value => console.log("fulfilled with value", value))
  .catch(error => console.log("rejected with error", error))
  .finally(() => console.log("finally"))

This code defines an asynchronous function example() that returns a promise object.

The example function creates a new promise by passing a callback function to the Promise constructor. The callback function (known as the executor function) takes two arguments: resolve and reject. These are the functions that can be used to either fulfill or reject the promise, respectively.

The code then uses the setTimeout function to perform an asynchronous operation. This function waits for 1000 milliseconds, then generates a random number to determine if it should fulfill or reject the promise. If the random number is less than 0.5, the promise is fulfilled with a value of 24. If the random number is greater than or equal to 0.5, the promise is rejected with an error message of "failed".

Finally, the code chains a .then method to the promise object returned by example(). This method is called when the promise is fulfilled, and it logs the message "fulfilled with value" and the value of the promise.

The code also chains a .catch method to the promise object. This method is called when the promise is rejected, and it logs the message "rejected with error" and the error message.

The code also chains a .finally method to the promise object. This method is called when the promise is either fulfilled or rejected, and it logs the message "finally".

Now you should have a fundamental understanding of how to use JavaScript promises for handling asynchronous operations.

Join the Newsletter