Promises in JavaScript (ES6)
A Promise in JavaScript is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises provide a cleaner, more readable way to handle asynchronous tasks compared to traditional callback functions, which can lead to “callback hell” when chaining multiple asynchronous operations.
Key Concepts of Promises:
- States of a Promise:
- Pending: The initial state, neither fulfilled nor rejected. The operation is still ongoing.
- Fulfilled: The operation completed successfully, and the promise has a value (a result).
- Rejected: The operation failed, and the promise has a reason (an error).
- Promise Structure:
A Promise is created using thePromise
constructor, which takes a function (executor) as an argument. This executor function has two parameters,resolve
andreject
, which are used to transition the Promise from the pending state to either fulfilled or rejected.
const myPromise = new Promise((resolve, reject) => { // Asynchronous operation (e.g., fetching data) let success = true; if (success) { resolve("Operation was successful!"); } else { reject("Operation failed."); } });
- Consuming Promises:
Once a Promise is created, you can attach handlers to it using.then()
,.catch()
, and.finally()
:
.then(onFulfilled, onRejected)
:- Used to handle the fulfilled state. The first function passed to
.then()
is executed when the promise is fulfilled, and the second function is executed if the promise is rejected.
myPromise.then( (result) => { console.log(result); // Operation was successful! }, (error) => { console.log(error); // Operation failed. } );
The.then()
method returns a new promise, allowing for chaining:myPromise .then((result) => { console.log(result); return "Next operation"; }) .then((nextResult) => { console.log(nextResult); // Next operation });
- Used to handle the fulfilled state. The first function passed to
.catch(onRejected)
:- Used to handle the rejected state. It’s essentially a shorthand for
.then(null, onRejected)
:
myPromise .then((result) => { console.log(result); }) .catch((error) => { console.log(error); // Operation failed. });
- Used to handle the rejected state. It’s essentially a shorthand for
.finally(onFinally)
:- Executes a callback once the promise is settled, regardless of whether it was fulfilled or rejected. This is useful for cleaning up resources or updating the UI after an operation:
myPromise .then((result) => { console.log(result); }) .catch((error) => { console.log(error); }) .finally(() => { console.log("Promise is settled (fulfilled or rejected)."); });
- Chaining Promises:
- Promises can be chained to perform a series of asynchronous operations in sequence. Each
.then()
returns a new promise, allowing further chaining.
myPromise .then((result) => { console.log(result); return new Promise((resolve) => resolve("Another operation")); }) .then((newResult) => { console.log(newResult); }) .catch((error) => { console.error("An error occurred:", error); });
- Promise.all()
Promise.all()
takes an array of promises and returns a single promise that resolves when all of the promises have resolved or rejects if any of the promises are rejected.
const promise1 = Promise.resolve(3); const promise2 = 42; const promise3 = new Promise((resolve) => { setTimeout(resolve, 100, "foo"); }); Promise.all([promise1, promise2, promise3]).then((values) => { console.log(values); // [3, 42, "foo"] });
- Promise.race()
Promise.race()
returns a promise that resolves or rejects as soon as one of the promises in the array resolves or rejects.
const promise1 = new Promise((resolve) => setTimeout(resolve, 500, "one")); const promise2 = new Promise((resolve) => setTimeout(resolve, 100, "two")); Promise.race([promise1, promise2]).then((value) => { console.log(value); // "two" (because it was resolved faster) });
- Promise.allSettled()
Promise.allSettled()
waits for all the promises to settle (either fulfilled or rejected) and returns an array of objects describing the outcome of each promise.
const promise1 = Promise.resolve("Success"); const promise2 = Promise.reject("Error"); Promise.allSettled([promise1, promise2]).then((results) => results.forEach((result) => console.log(result.status)) ); // Output: // "fulfilled" // "rejected"
- Promise.any()
Promise.any()
takes an array of promises and returns a single promise that resolves as soon as any of the promises in the array fulfill. If all of the promises are rejected, it returns a promise that rejects with anAggregateError
(a special error object that stores multiple error objects).
const promise1 = Promise.reject(0); const promise2 = new Promise((resolve) => setTimeout(resolve, 100, "quick")); const promise3 = new Promise((resolve) => setTimeout(resolve, 500, "slow")); Promise.any([promise1, promise2, promise3]).then((value) => { console.log(value); // "quick" }).catch((err) => { console.log(err); });
Why Use Promises?
- Better readability: Promises provide a more readable and maintainable way to handle asynchronous code compared to nested callbacks.
- Error handling: Errors are propagated down the chain, and you can catch them with a single
.catch()
at the end. - Composability: Promises can be chained and combined in various ways (
Promise.all
,Promise.race
, etc.), making them flexible and powerful for handling complex asynchronous workflows.
Promises are a fundamental part of modern JavaScript, especially in the context of working with APIs, asynchronous operations, and complex workflows. They are often used with async
/await
syntax in modern JavaScript to make asynchronous code even more readable.