In JavaScript, a promise is an object representing some value that may not yet be available, but will be available at some point in the future. We can leverage these promises to handle asynchronous operations.


To create a promise, we can call the Promise constructor with the new operator:

const promise = new Promise((resolve, reject) => {
 // business logic

The Promise constructor takes in just a single parameter: an executor function. This function accepts two optional parameters: resolve and reject. Consider the following example:

const p = new Promise((resolve, reject) => {
 // business logic

 if (/* success condition */){
   resolve(/* optional arguments */);
 } else {
   reject(/* optional arguments */);

In the above snippet, we first perform any operations and business logic required. Then, if some condition was successful, we call resolve with any arguments needed. If the condition was unsuccessful, we call reject (again, along with any arguments needed).

Promise states

Every promise has an action, or specific objective to carry out (e.g., fetching data from an API, delaying execution with setTimeout, etc.). Depending on the status that action, a promise can exist in exist in one of three states:

  • fulfilled: the promise’s action has succeeded
  • rejected: the promise’s action has failed
  • pending: the promise has been neither fulfilled nor rejected

It is important to note that a promise cannot change its state from fulfilled to rejected (and vice versa). When a promise has been either fulfilled or rejected, the promise is considered settled

Now that we’ve seen what promises look like and which states they can live in, how do we go about consuming a promise? That is, what are some ways that we can actually handle a “fulfilled” state or a “rejected” state?

Handling promise states

Promise objects have two built-in methods to handle their state: the .then() method to handle a promise’s fulfilled state, and .catch() to handle its rejected state. Both methods return a new promise object themselves, making the methods chainable (more on this later). 

The first of these methods, .then(), accepts two parameters: onFulfilled and onRejected, the latter of which is optional. These are functions to invoke if the promise is fulfilled or rejected, respectively. Consider the following example:

const promise = new Promise((resolve, reject) => {
 resolve("Message: Success!");

promise.then((value) => {
 console.log(value); // "Message: Success!"

When the promise resolves successfully and reaches a fulfilled state, we can handle that state through the promise’s .then() method. We do so by registering a callback function (i.e., our onFulfilled function) to use the resolved string (i.e., "Message: Success!), then print it to the console.

In the case of a rejected state, we can use the second, optional parameter passed in to .then() (i.e., onRejected). However, should there be any errors, the onRejected callback would be unable to handle it. As such, a more leveraged way is to call .catch().

The .catch() method accepts a single onRejected parameter. This callback is invoked if any promise in the chain is rejected. Consider the following example:

const promise = new Promise((resolve, reject) => {
 throw new Error("Something went wrong");

promise.catch((error) => {
 console.error(error); //  "Something went wrong"

In the case of an error as seen  above, the promise enters a rejected state. Through the use of the promise’s built-in .catch() method, we register a callback to handle that error, which simply logs the error to the console.

Putting it all together

Recall that earlier we mentioned that both the then() and .catch() methods each return a new promise themselves. This makes it possible for us to chain such operations:

const promise = new Promise((resolve, reject) => {
 reject("Promise is rejected");

 .then((result) => {
 .catch((error) => {

// Output: "Promise is rejected"

In this example, we use .then() to log the result of our promise when it is fulfilled, and .catch() to log any errors that occur when it is rejected.

With all this in mind, we’ll explore where the capabilities of promises really shine: handling asynchronous operations.

Asynchronous operations

In JavaScript, code is executed on a single thread. That is, any callback functions that we register to handle the promise state still run on that main thread. However, using a promise allows us to create asynchronous operations (e.g., making an API request) that run “outside” of our local environment. The main thread can continue executing while waiting for such operations to respond. In turn, any callback functions that we register to handle the response are called asynchronously.

Consider the following example:

 .then((response) => response.json())
 .then((json) => console.log(json));

We first use fetch to make a GET request to our API endpoint (more on JSONPlaceholder here). The method returns a promise that we can resolve into a Response object. We handle it with the first .then() in the chain by parsing its JSON input (i.e., response) into a JavaScript object literal. Since response.json() returns a promise, we handle it with the second .then() by simply logging the object to the console. 

As a result, we see the following outputted to the console, which represents the response from the API:

 "userId": 1,
 "id": 1,
 "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
 "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"

Learn more about promises

Promises are a great way to handle asynchronous operations. In addition to handling promise states with then() and catch(), you may also want to consider async / await as well.

For more information about promises, check out these resources:

Or explore the Intermediate JavaScript Nanodegree program to master the most popular programming language in the world.