Being a front end developer, I only recently realized that promises are not universal. I had never given them much thought and figured like variables or other common code concepts, all languages had them, but apparently no. Recently, I have been mentoring some backend developers and was surprised to learn that their back end code doesn't have them. With these developers in mind, I figured it would nice to have a cheat sheet on how promises work and how to use them.
Before we get into the nitty gritty about how promises work, let's first define them:
Promiseobject represents the eventual completion (or failure) of an asynchronous operation and its resulting value. (mdn web docs)
Promises are special because they allow us to perform an asynchronous call and then wait for their results such as when using
fetch to get data from an API as we do in Listing 1 which returns the weather forecast for Indianapolis.
If I want to continue building on my result, I can daisy chain the
.then(), for example listing 2 converts and returns the results from the fetch into JSON and the second
.then() logs the resulting JSON to the console.
But what happens if our call fails? To handle errors we can add a
catch() to our promise chain. A catch will allow us to decide what to do with the error and potentially give us an opportunity to recover from the error or at least handle the error gracefully.
The catch in this context, would trigger regardless of where in our chain the error happened. If we want code to execute regardless of a success or failure, we can use
finally(). In the
getForecast() function bellow, regardless of whether the fetch is successful, loading will be set to false once the chain is finished executing.
Let's refactor our code to use the equivalent
async/await and the built in try/catch/finally error handling pattern. Depending on your goal, the number of promises, and personal preference one may be easier to read then the other. Furthermore it is worth noting that they can be used together.
The functions from Listing 4 and 5 are functionally identical. Let's expand our example to use 2 promises and get the weather from Indianapolis and from Chicago.
Although the code in listing 6 will return both weather forecasts, it's not very efficient. We first wait until we get Indianapolis' forecast before we get Chicago's. Written using
.then() it would look as follows:
We don't want to wait for the first in order to fetch the second. We can therefore use
Promise.all() in order to get both in parallel.
Our parameter is an array of promises, and
Promise.all() returns an array of values equal in length to the number of promises supplied.
Although we have made the code more efficient, if either of the forecast calls fails, we won't get any values for any of our promises in the
Promise.all() even if some were successful. To prevent the entire
Promise.all() from failing if one fails, we can add a catch to each promise. Listing 7 returns
null if the promise throws an error.
Another option for handling errors when we are calling multiple promises in parallel is through the use of
Instead of the promise values,
Promise.allSettled() returns an array of objects with 2 properties.
If the promise resolved, the returned object for the promise will have a
status property with a value of
fulfilled, and a
value property with the promise's value.
If the promise errored, then it will have a
status property with a value of
rejected, and a
reason property with the reason the promise was rejected.
The benefit of using
Promise.allSettled() is that we still get values for the promises that resolved without having to add catches to each individual promise in the array. The downside is that we cannot handle our errors with the use of a catch like the previous examples in this article. We must check each value individually, and handle our successes and errors accordingly as seen in Listing 10.
Both listing 9 and 10 achieve the same results, we get Indianapolis' and Chicago's weather forecasts in parallel and if either of the calls fail, we handle the error by returning a null value for that forecast. Which is a better solution depends on your use case is and how you want to handle your errors.
But what if we don't care which one we get? If we are of the opinion that the cities are close enough together and that which ever forecast returns first is good enough we have 2 options:
Promise.any(). Let's look at
Promise.race() we will get only 1 result which will be the value of the first promise to resolve. If the first promise to return a value throws an error, the
Promise.race() will error.
The downside are:
- we don't know which of the 2 promises the data came from
- if the first to return fails, then the Promise fails and an error is thrown
If we want to wait until something succeeds (even if there are failures) we can use
Promise.any() will ignore failures until all of the promises have failed only then will the catch trigger.
Even though we have a rejection in listing 12, the rejected promise will be ignored and either the Indianapolis or Chicago forecast will be returned, which ever is fastest.
We've looked at calling promises all sorts of ways but what if the function we are dealing with isn't a promise but a callback? How would we turn a callback into a promise we can await or chain?
Let's look at how we would transform
setTimeout() into a promise.
We wrap the function inside of a newly created promise and upon the completion of the code we call
resolve() passing in our results as the parameter.
Another common use case is the File Reader callback we often use for file uploads:
To turn the callback into a promise, we create a new Promise, which resolves when the callback returns its value, or rejects if the callback errors.
printFile() function returns a promise and we can use it with any of the techniques presented in this article.