DEV Community

Cover image for Learning JavaScript Promises
marlonry
marlonry

Posted on • Updated on

Learning JavaScript Promises

Hello, I am going to give you a quick introduction about JavaScript promises and why they work the way they do. Promises have been around for a while now. It is really important to understand this topic as modern development revolves around asynchronous code. Let's start by defining what they are:

What are Promises?

In JavaScript, a Promise is an object that returns some type of value that will arrive at any point in the future. During the process, a Promise will start in the pending state, which informs you that it hasn't been completed and that it will eventually return a value. This returned value can either be in a resolved state (success) or rejected state (fail), after it's been consumed.

It's really important to understand the three main states of a Promise.

  • Pending: The Promise is still doing its work and we don't know the type of response yet. We just know we have been promised a value.
  • Resolved: The promised value has been delivered successfully.
  • Rejected: The promised value has not been delivered successfully and we receive an explanation as to why it was rejected.

Now let's put this information into practice.

Let's create a Promise

Let's start with a simple example. In the image below, we create and consume a Promise right away.

const isPromisedFullfilled = true;

const myPromise = () => {
  return new Promise((resolve, reject) => {
    if (isPromisedFullfilled) {
      resolve("Hello, this is a successful Promise");
    }
    reject("Hello, this is a rejected Promise");
  });
};

console.log(myPromise()); // Promise {<pending>}

myPromise()
  .then((result) => console.log(`Success: ${result}`)) // if true = resolved
  .catch((err) => console.log(`Error: ${err}`)); // if false = rejected

// Output: Success: Hello, this is a successful Promise
Enter fullscreen mode Exit fullscreen mode

Now let's break down each part of the example above. A function called myPromise returns a Promise. Inside the myPromise function, we get access to the resolve and reject methods on the arguments. These methods allow you to resolve or reject a value. Once the promise is consumed, this will define whether the promise has been fulfilled or not. In this case, we have a variable called isPromisedFullfilled, which has a boolean value and when the promise is consumed, it will resolve or reject depending on the variable value.

If the isPromisedFullfilled equals true, it will return a message "Hello, this is a successful Promise" and if it is false, it will run the next line of code which will reject the promise and return a message "Hello, this is a rejected Promise". This will happen once the promise has been consumed.

const isPromisedFullfilled = true;

const myPromise = () => {
  return new Promise((resolve, reject) => {
    if (isPromisedFullfilled) {
      resolve("Hello, this is a successful Promise");
    }
    reject("Hello, this is a rejected Promise");
  });
};
Enter fullscreen mode Exit fullscreen mode

At the creation state, we can see that the Promise is still in a pending state when we log myPromise to the console.

console.log(myPromise()); // Promise {<pending>}
Enter fullscreen mode Exit fullscreen mode

Let's handle the Promise

In order to consume a Promise, we get access to the .then() method that accepts two callback functions - one for the success and failure case of the promise. However, usually, the failure case of a promise is handled with the .catch() method, which only accepts one callback function to handle the rejected state or a thrown error.

As seen below, .then() method will handle the resolved state and provide a result, and .catch() will handle the rejected state and provide reasons for the errors. In this case, isPromisedFullfilled equals true, so "Success: Hello, this is a successful promise" will be the result.

myPromise()
  .then((result) => console.log(`Success: ${result}`)) // if true = resolved
  .catch((err) => console.log(`Error: ${err}`)); // if false = rejected

// Output: Success: Hello, this is a successful Promise
Enter fullscreen mode Exit fullscreen mode

Why Promises?

Promises were made to handle asynchronous operations in an easier way as well as solving the "Callback Hell", which occurs when nesting functions inside other functions. We can usually see this pattern developing when dealing with asynchronous programming but with the introduction of Promises, we only attach a .then() after another. If we were to convert the example above to "Callbacks", it would look something like this:

let done = false;

function doSomething(successCallback, errorCallback) {
  if (done) {
    successCallback("Hello, this is a successful result");
  } else {
    errorCallback("Hello, this is a failed result");
  }
}

doSomething(
  (result) => console.log(`Success: ${result}`),
  (err) => console.log(`Error: ${err}`)
);
Enter fullscreen mode Exit fullscreen mode

Although a few Callbacks don't seem like a big problem, once we start nesting them, using Callbacks can get out of hand really quickly.

Now that we know that Promises solve some problems, at the end of the day this is not the final solution to other problems that arise when using Promises, but it is important to understand them in order to move on to other ways of handling Asynchronous code like Async/Await.

Handling Multiple Promises

There are some important Static methods that can help us handle multiple Promises at once, for different cases, These are:

  1. Promise.all()
  2. Promise.allSettled()
  3. Promise.race()
  4. Promise.any()

I will explain each one briefly.

Promise.all()

This method takes an array of Promises as an argument and waits until all of the Promises are resolved. Once that's done, it will return a Promise where we can access an array with all the results from the resolved Promises through a .then() method.

const p1 = new Promise((resolve, reject) => {
  resolve("This is the first Promise"); // resolves
});

const p2 = new Promise((resolve, reject) => {
  resolve("This is the second Promise"); // resolves
});

Promise.all([p1, p2])
  .then((result) => console.log(result))
  .catch((err) => console.log(err));

// Output: 
// ["This is the first Promise", "This is the second Promise"]
Enter fullscreen mode Exit fullscreen mode

In case that one of them rejects, it will only return the reason for the first rejected Promise. As shown below.

const p1 = new Promise((resolve, reject) => {
  resolve("This is the first Promise"); // resolves
});

const p2 = new Promise((resolve, reject) => {
  reject("This is the second Promise"); // rejects
});

const p3 = new Promise((resolve, reject) => {
  reject("This is the third Promise"); // rejects
});

Promise.all([p1, p2, p3])
  .then((result) => console.log(result))
  .catch((err) => console.log(err));

// Output: "This is the second Promise"
Enter fullscreen mode Exit fullscreen mode

Promise.allSettled()

This method is similar to Promise.all(). It also takes an array of Promises as an argument, but the difference is that it returns a resolved Promise after all the Promises have either been resolved or rejected. After handling the returned Promise with .then(), we get access to an array of objects with the information about each Promise.

const p1 = new Promise((resolve, reject) => {
  resolve("This is the first Promise"); // resolves
});

const p2 = new Promise((resolve, reject) => {
  reject("This is the second Promise"); // rejects
});

const p3 = new Promise((resolve, reject) => {
  reject("This is the third Promise"); // rejects
});

Promise.allSettled([p1, p2, p3])
  .then((results) => console.log(results));

// Output: [Object, Object, Object]
Enter fullscreen mode Exit fullscreen mode

As seen in the example above, we get an array of objects. After looping through the results and logging the results to the console, we can see the objects with useful information about each Promise.

Promise.allSettled([p1, p2, p3])
  .then((results) => {
    results.forEach((result) => {
      console.log(result)
    })
  })

// Output: 
// {status: "fulfilled", value: "This is the first Promise"}
// {status: "rejected", reason: "This is the second Promise"}
// {status: "rejected", reason: "This is the third Promise"}
Enter fullscreen mode Exit fullscreen mode

Promise.race()

This method takes an array of Promises and returns a fulfilled Promise as soon as any Promise resolves or rejects. In the example below, the third promise resolves after a second, therefore its result will be handled on the .then(), in case that a promise rejects first, the error will be handled on the .catch();

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("This is the first Promise"), 3000); 
  // resolves after 3 seconds 
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => reject("This is the second Promise"), 2000); 
  // rejects after 2 seconds 
});

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("This is the third Promise"), 1000); 
  // resolves after 1 second
});

// Promise.race()
Promise.race([p1, p2, p3])
  .then((result) => console.log(result))
  .catch((err) => console.log(err));

// Output: "This is the third Promise"
Enter fullscreen mode Exit fullscreen mode

Promise.any()

This method is basically the opposite of Promise.all(), In Promise.any() if all promises are rejected it will return an AggregateError as seen below.

const p1 = new Promise((resolve, reject) => {
  reject("This is the first Promise"); // rejects
});

const p2 = new Promise((resolve, reject) => {
  reject("This is the second Promise"); // rejects
});

Promise.any([p1, p2])
  .then((result) => console.log(result))
  .catch((err) => console.log("Error: " + err));

// Output: "Error: AggregateError: All promises were rejected"
Enter fullscreen mode Exit fullscreen mode

And when the Promises resolve, it will return a Promise with the resolved value from the Promise that fulfilled the fastest. In the example below, the Promise that took only a second to resolve will be the result of the handled Promise in the .then() method.

const p1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000, "This is the first Promise"); 
  // resolves after 2 seconds
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "This is the second Promise"); 
  // resolves after 1 second
});

Promise.any([p1, p2])
  .then((result) => console.log(result))
  .catch((err) => console.log(err));

// Output: "This is the second Promise"
Enter fullscreen mode Exit fullscreen mode

Promises are a very interesting part of javascript as they offer various features to work with asynchronous tasks. Even though in newer versions of javascript, there are better ways to deal with asynchronous programming, it is really important to understand how Promises work.

That's it for me today! As a reminder, this guide is based on the things I've learned about promises and how I understand them. Make sure to leave your feedback on things I could improve, and I hope it will also be useful for somebody that's learning Promises. See you guys. Catch you in the next one!!! 😃

Top comments (0)