Web Dev Simplified Blog

The Complete JavaScript Promise Guide

September 13, 2021

Promises in JavaScript look confusing at first but I promise you by the end of this article you will be a master of promises.

If you prefer to learn visually, check out the video version of this article.

What Is A Promise

A promise in JavaScript is very similar to a promise in real life. Imagine that you loan your friend Jim $100 to buy something. When you loan him the money he promises you that he will pay you back in full after work the next day. You now have Jim’s promise that as long as nothing bad happens he will pay you back in the future after he finishes work. If something unexpected happens, though, and Jim breaks his leg at work then of course he will not be able to fulfill his promise and you will not get paid back on time.

This is exactly how promises are handled in JavaScript. A promise is just some set of code that says you are waiting for some action to be taken (Jim finishing work) and then once that action is complete you will get some result (Jim paying you back). Sometimes, though, a promise will be unfulfilled (Jim breaking his leg) and you will not get the result you expect and instead will receive a failure/error.

If you are familiar with callbacks then you may realize that promises solve a similar problem to callbacks, but they do so in a much more elegant way.

Implementing Promises

Below is an example of how to write the above scenario using callbacks.

function handleJimWork(successCallback, errorCallback) {
   // Slow method that runs in the background
  const success = doJimWork()
  if (success) {
    successCallback()
  } else {
    errorCallback()
  }
}

handleJimWork(() => {
  console.log('Success')
}, () => {
  console.error("Error")
})

In this example we have a handleJimWork function that takes in a callback for what to do on success and failure. We then run the doJimWork function which is a slow function that runs in the background. This would be similar to doing something like a fetch request to get information from a server. Then based upon the result of running this slow background function we get a result of either true or false depending on if Jim was able to successfully get through the work day. Depending on that value we either call the success or error callback. Then when we call handleJimWork we pass in both a success and error function which will run depending on the success of doJimWork.

Now let’s look at how we convert this to use promises.

function handleJimWork() {
  return new Promise((resolve, reject) => {
    // Slow method that runs in the background
    const success = doJimWork()
    if (success) {
      resolve()
    } else {
      reject()
    }
  })
}

const promise = handleJimWork()
promise.then(() => {
  console.log('Success')
}).catch(() => {
  console.error("Error")
})

You will immediately notice that the code is very similar, but with one big change. Instead of passing callbacks to handleJimWork we instead are using the reject, and resolve methods of a promise. We are also returning a promise from handleJimWork and then when we call handleJimWork we are using that promise by calling the .then and .catch methods on the promise.

First, lets start by breaking down what is happening in handleJimWork. If you want to convert a function to use promises you need to always return a promise from that function since you need access to a promise object to check if the promise was successful or not. This is similar to Jim giving you his promise that he will pay you back after work. When we create this promise object it takes a function with two parameters: resolve and reject. These parameters are functions that correlate with a success and failure state.

The resolve function is the success function and should be called whenever the promise was successful. This replaces our successCallback.

The reject function is the error function and should be called whenever the promise was not able to be completed successfully. This replaces our errorCallback.

The next big difference in these examples is how we call handleJimWork. In the callback version we just passed the callbacks to handleJimWork, but in the promise example we don’t actually pass any callbacks to handleJimWork. Instead we use the promise returned from handleJimWork to check for success/failure. On the promise we call .then and .catch to check for success or failure.

If the promise in handleJimWork calls the resolve method then all of the code in .then is run. This is why we put the successful callback in the .then.

If the promise in handleJimWork calls the reject method then all of the code in .catch is run. This is why we put the error callback in the .catch.

The best way to think of promises is to just think of resolve as the same as .then and reject as the same as .catch. It also helps to think of promises in terms of plain English.

Jim promises to go to work and if this resolves successfully then he will pay us $100. If for some reason the promise is rejected by Jim not going to work, breaking his leg, or some other reason, then we need to catch that failure and be prepared to handle it accordingly.

One last thing to know about .then and .catch is that you can actually pass a parameter down to each.

function handleJimWork() {
  return new Promise((resolve, reject) => {
    // Slow method that runs in the background
    const success = doJimWork()
    if (success) {
      resolve(100)    } else {
      reject("Jim broke his leg")    }
  })
}

handleJimWork().then(amount => {  console.log(`Jim paid you ${amount} dollars`)}).catch(reason => {  console.error(`Error: ${reason}`)})

In the above example I modified our code slightly so that now the resolve and reject methods are actually called with a parameter. In the case of resolve we pass in the amount of money that Jim pays us back and in the reject case we pass in the reason why Jim cannot pay us back. Then in .then and .catch we use the parameters passed to resolve and reject to give us more detailed information about what happens. You may also notice I simplified our code a bit by not extracting the result of handleJimWork into its own promise variable. In most cases when you write promises you will just directly chain .then and .catch onto the end of the function instead of creating a variable to store the promise in.

Promise Chaining

Just by looking at the above examples promises may not seem that great, but the real power of promises comes in the ability to chain them together which solves the problem of callback hell.

function one(callback) {
  doSomething()
  callback()
}

function two(callback) {
  doSomethingElse()
  callback()
}

function three(callback) {
  doAnotherThing()
  callback()
}

one(() => {
  two(() => {
    three(() => {
      console.log("We did them all")
    })
  })
})

In the above example we have three functions that all do something and we need to call them in order so once the first function finishes we call the second and so on. Then finally at the end we log out that they have all three finished. This is a common problem in JavaScript and with callbacks you start to run into a nested mess as you can see. With promises, though, there is no nested mess to worry about since you can chain promises.

function one() {
  return new Promise(resolve => {
    doSomething()
    resolve()
  })
}

function two() {
  return new Promise(resolve => {
    doSomethingElse()
    resolve()
  })
}

function three() {
  return new Promise(resolve => {
    doAnotherThing()
    resolve()
  })
}

one().then(() => {
  return two()
}).then(() => {
  return three()
}).then(() => {
  console.log("We did them all")
})

In the above example we converted one, two, and three to promises and then we just chain together each .then of the previous promise into the next. That is because if you return a promise from a .then or .catch that promise will be used with the next .then or .catch in the chain.

In this example we are calling one and in the first .then we are calling two and returning the promise two returns. Since we are returning a promise from a .then JavaScript is smart enough to run the code in that promise and once it finishes call the next .then in the chain. This is repeated again with three and we finally get the log printed at the end. We can even clean this up a bit further.

one()
  .then(two)
  .then(three)
  .then(() => {
    console.log("We did them all")
  })

The above code works exactly the same since the functions two and three return promises when called.

Advanced Promise Features

So far we have covered just the most basic use cases for promises, but there is much more you can do with promises.

.finally

The .finally method works very similar to .then and .catch in that it is chained onto a promise, but the code in .finally will run whether the promise fails or succeeds.

handleJimWork()
  .then(amount => {
    console.log(`Jim paid you ${amount} dollars`)
  }).catch(reason => {
    console.error(`Error: ${reason}`)
  }).finally(() => {    console.log("This always runs")  })

.finally is great if you need to do some clean up or you want to do the same thing whether a promise succeeds or fails.

Promise.all

The rest of the methods in this section will all be on the Promise object itself. The Promise.all method takes an array of promises and will wait for all of them to resolve before calling .then with the results of all the promises. If any of the promises reject, though, it will immediately call .catch with the error of the failed promise.

function one() {
  return new Promise(resolve => {
    doSomething()
    resolve("From One")
  })
}

function two() {
  return new Promise(resolve => {
    doSomethingElse()
    resolve("From Two")
  })
}

Promise.all([
  one(),
  two()
]).then(messages => {
  console.log(messages)
  // ["From One", "From Two"]
}).catch(error => {
  // First error if any error
})

Promise.allSettled

This method is very similar to Promise.all. The only difference is that Promise.allSettled will wait for all promises to succeed and/or fail before calling .then. Promise.allSettled also never calls .catch and instead will tell you if each promise failed or succeeded in the .then.

Promise.allSettled([
  one(),
  two()
]).then(messages => {
  console.log(messages)
  /* [
    { status: "fulfilled", value: "From One" },
    { status: "fulfilled", value: "From Two" }
  ] */
})

Promise.any

This method takes an array of promise just like the previous methods, but it will only wait for one promise to resolve. Once one promise in the list is successful it will call .then with the result of the first successful promise.

Promise.any([
  one(),
  two()
]).then(firstMessage => {
  console.log(firstMessage)
  // Message from whichever resolved first
}).catch(error => {
  // Generic error saying all promises failed
})

Promise.race

This method is very similar to Promise.any, but it only waits until one promise either fails or succeeds unlike Promise.any which only cares about the first success. Promise.race will wait until the first promise fails or succeeds and then call .then or .catch accordingly.

Promise.race([
  one(),
  two()
]).then(firstMessage => {
  console.log(firstMessage)
  // Message from first promise to finish if it was a success
}).catch(firstError => {
  // Message from first promise to finish if it was an error
})

Promise.resolve

This method is a shorthand for returning a promise that resolves immediately. This is useful if you need to pass a promise to something but do not already have a promise.

Promise.resolve(200).then(amount => {
  console.log(amount)
  // 200
})

Promise.reject

This method is the same as Promise.resolve, but for returning a failing promise.

Promise.reject("Error").catch(message => {
  console.error("Error")
  // Error
})

Conclusion

Promises are incredibly versatile and much easier to work with then callbacks. This is why any time I need to deal with async code I always reach for a promise over a callback.


Kyle Cook
Kyle Cook