Thanks to the asynchronous part of JavaScript, we have a couple of features that allow us to run non-blocking chunks of code, which means other parts of our code get executed without delay. One such feature and the concern of this article is the promise object, Introduced in ES6 (ES2015), it sure has made asynchronous programming in JavaScript a lot neater to look at and easy to understand.
A promise is an object that represents the eventual success or failure of an asynchronous operation. Callbacks are attached to the returned object to handle the eventual result.
Why Promises?
Before promise got introduced, we used to pass callbacks in functions, which was helpful, but with it came the Callback Hell (heavily nested callback code that is difficult to read). Once you start doing several asynchronous operations in a row, passing callbacks in functions usually led to a Pyramid of doom. With the promise object, working with asynchronous tasks has gotten a lot more efficient and readable.
Creating a Promise
The Promise()
constructor is used to create a new promise object but it is primarily used to wrap old APIs that don't already support promises (i.e setTimeout()
) since they still expect success and failed callbacks to be passed the old fashioned way.
'use strict';
const myFirstPromise = new Promise ((resolve, reject) => {
/* resolve(...) in the case of a success */
/* reject(...) in the case of a failure */
});
State of a Promise
A promise can be of three (3) states:
Pending: This is the initial state immediately after instantiation. The result will be undefined.
Fulfilled: Indicates the promise is resolved (success case). In this case, the result will be a value.
Rejected: This is a failed-to-resolve case. The result will be an error object.
Note: When there's an error handler ( in the
.catch()
block or an onRejected callback in a.then()
block) attached to your promise, the state of the promise in the case of an error will be fulfilled not rejected because the error is passed as a value to the error handler.
Promise methods
We have two (2) categories of methods that can be used along with a promise; the instance methods and the static methods.
Instance methods
Promise.then()
This appends two (2) callbacks to a promise, one for the case of success and another for the case of a failure. Both callbacks are optional which means your .then()
block can hold either of the two if not both depending on the purpose of your .then()
block.
myFirstPromise.then(
(value) => { /* code for the case of success */ },
(error) => { /* code for the case of failure */ }
);
Promise.catch()
It carries an error handler callback that is called in the case of a failure. It returns a new promise resolving to the value of the callback if it is called or its original value when the promise is fulfilled. The .catch()
block is ignored when the promise it is appended to passes a success value as a result.
myFirstPromise
.then((value) => console.log(value))
.catch((error) => console.error("Oops Something went wrong"));
Promise.finally()
It returns a new promise that is resolved when the original promise is resolved. It carries a handler that is called when the promise is settled, whether fulfilled or rejected.
myFirstPromise
.then((value) => console.log(value))
.catch((error) => console.log("Oops Something went wrong"))
.finally((info) => console.log("All done"));
Static methods
Promise.resolve(value)
This method returns a promise object with a given value. If the value is a promise, the promise is returned; if the value is a thenable (has a .then()
method), the promise is going to resolve according to the value of the .then()
method, otherwise, the returned promise will be fulfilled with the value.
/* Promise.resolve() with a hardcoded string value */
const myFirstPromise = Promise.resolve("success");
/* Using the .then() method to return a new promise based on the initial resolved value */
myFirstPromise
.then((value) => console.log(value + "!!!"));
/* Expected output: success!!! */
Promise.reject(reason)
This method returns a new promise object that is rejected with a given reason.
/* Promise.reject() with a hardcoded string value */
const myFirstPromise = Promise.reject("failed")
/* Using the .catch() method to append an error handler */
myFirstPromise
.catch((errorMessage) => console.error(errorMessage));
/* Expected output: failed */
Promise.all(iterable)
This method takes in an iterable (usually an array) and returns a new promise. The promise is resolved when all listed promises are settled, and the aggregating array of their resulting values (in the same order as listed in the iterable of all the promises) becomes its result.
In the case of failure in any of the promises in the iterable, all other promises are ignored. This is most useful in cases where we need an "all success" to move to the next phase of the code.
/* syntax */
Promise.all([promise, promise, promise,...])
.then(/* onFulfilled callback */, /* onRejected callback */)
/* Example */
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("success-1"), 2000)
})
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("success-2"), 5000)
})
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("success-3"), 3000)
})
Promise.all([promise1, promise2, promise3])
.then(values => console.log(values))
.catch(() => console.error("something went wrong"))
/* Expected outcome: ["success-1", "success-2", "success-3"] */
Promise.allSettled(iterable)
This is a recent addition to JavaScript at the time of writing this article, not all browsers have support for it. You can check caniuse to find out the browsers with support for it.
It waits till all promises are settled regardless of their results (resolved or rejected). It returns a promise with an array of objects that each describe the outcome of each promise.
/* Using the listed promises from the example for Promise.all() */
Promise.allSettled([promise1, promise2, promise3])
.then(values => console.log(values))
/* Expected outcome:
{
{status: "fulfilled", value: "success-1"},
{status: "fulfilled", value: "success-2"},
{status: "fulfilled", value: "success-3"}
}
*/
Promise.any(iterable)
It takes an iterable of Promise objects and, as soon as one of the promises in the iterable is fulfilled, it returns a single new promise that resolves with the value from that promise.
It returns only the first resolved promise, and the rest of the promises are ignored.
In a case where all promises in the iterable are rejected then the returned promise is rejected with the AggregateError
– a special error object that stores all promise errors in its errors property.
/* Using the listed promises from the example for Promise.all() */
Promise.any([promise1, promise2, promise3])
.then(values => console.log(values))
/* Expected outcome: success-1 */
Promise.race(iterable)
Similar to the Promise.any()
method but unlike the Promise.any()
, it waits for the first promise in the iterable that gets settled whether resolved or rejected.
Words to take note: Settled is either resolved or rejected. Resolved is successful, Rejected is failed.
When the returned promise gets resolved it will be with the value of the first promise in the iterable to be resolved and when the returned promise is rejected it will be rejected with the reason from the first promise that was rejected in the iterable.
/* Using the listed promises from the example for Promise.all() */
Promise.race([promise1, promise2, promise3])
.then(values => console.log(values))
/* Expected outcome: success-1 */
/* Even though all got settled(resolved or rejected), promise1 got settled first */
Chaining
One of the common needs when running asynchronous operations is usually the need to run two or more operations back to back, where the result of each operation is dependent on the outcome of the previous one. Before the introduction of promises doing this often led to a Pyramid of doom. With promises, all we need to do is create a promise chain.
/* A typical pyramid of doom, resulting from passing callbacks in functions*/
doSomething((result) => {
doSomethingElse(result, (newResult) => {
doThirdThing(newResult, (finalResult) => {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
/* Attaching our callbacks to returned promises instead*/
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
Note: Always return results, otherwise callbacks won't catch the result of a previous promise, with arrow functions
() => x
is short for() => { return x; }
.
Error handling in Promises
Error handling in promises can be done in either of two possible ways.
- By appending an error handler to the promise chain with a
.then()
method.
/*Using earlier sample code as an example */
myFirstPromise
.then(
/* success case handler callback */,
/* error handler callback */
);
/* As earlier stated, `.then()` method can take both the unfulfilled and onRejected callbacks or either of both depending on how you choose to structure your code */
/*use case scenario*/
myFirstPromise
.then(
(value) => console.log(value),/* success case handler callback */
(error) => console.error(error)/* error handler callback */
);
/* In the use case scenario above I used the `console.error()` method to display the error value in the console. */
- By appending an error handler to the promise chain with a
.catch()
method
/*Using earlier sample code as an example */
myFirstPromise
.catch (
/*error handler callback goes here*/
);
/*use case scenario*/
myFirstPromise
.catch (
(error) => console.error(error) /* error handler callback */
);
/* In the use case scenario above I used the `console.error()` method to display the error value in the console. */
Handling error in a promise chain is much easier and cleaner appending a .catch()
method at the end of the promise chain as shown below.
const myPromise = new Promise(myExecutorFunc)
/* Adding the error handler in `.then()` method */
myPromise
.then(handleFulfilledA,handleRejectedA)
.then(handleFulfilledB,handleRejectedB)
.then(handleFulfilledC,handleRejectedC);
/* Using a single `.then()` method to handle any errors*/
myPromise
.then(handleFulfilledA)
.then(handleFulfilledB)
.then(handleFulfilledC)
.catch(handleRejectedAny);
Note: While it can be useful to continue the chaining after the
.catch()
block, any error that occurs after the.catch()
block won't be visible to it.
Conclusions
Performing asynchronous operations in JavaScript got a lot better with the use of promises and also paved the way for the almighty async-await
, so it is safe to say we can forget about the days of the popular callback pyramid of doom that were painful to see and read.
While we have mostly used promises to log values in the console in this article, they are usually used to get files or do XMLHttpRequest to and from the server, among others.
You can choose to learn more about promises with the MDN Web Docs and w3schools.
With that, I thank you for making it to the end of this article and I hope this helped you in a way or two because that is and will always be my number one goal in putting these up, and sharing with you guys.
Top comments (0)