JavaScript Promises: A Simple and Clear Guide for Beginners
Introduction
Imagine you are booking a flight online. You enter your destination, departure date, and preferred time. You click the search button and wait for the results. What happens next? You don't just sit there and stare at the screen, hoping for the best. You probably do something else, like browse other websites, listen to music, or check your messages. Meanwhile, the flight website is searching for the best deals, contacting the airlines, and filtering the options. When the results are ready, you get a notification, and you can choose your flight.
This is an example of asynchronous programming, which means doing multiple things at the same time, without blocking the main thread of execution. JavaScript is a single-threaded language, which means it can only do one thing at a time. However, it can use callbacks, events, and promises to handle asynchronous operations, such as fetching data from a server, reading a file, or performing a computation.
What is a promise?
A promise is an object that represents the eventual outcome of an asynchronous task. An asynchronous task is a task that can take some time to complete, and does not block the main thread of the program.
A promise can have one of the following states:
- Pending: the promise is not settled yet, and the outcome is unknown.
- Fulfilled: the promise is settled successfully, and the outcome is a value.
- Rejected: the promise is settled with an error, and the outcome is a reason.
How to create a promise
A promise can be created using the new Promise()
constructor, which takes a function as an argument. This function is called the executor, and it has two parameters:
resolve
: Theresolve
parameter is a function that can be called to fulfill the promise with a value.reject
: Thereject
parameter is a function that can be called to reject the promise with a reason.
For example, the following code creates a promise that simulates a flight search:
// Create a new promise
let flightPromise = new Promise((resolve, reject) => {
// Simulate a random delay
let delay = Math.floor(Math.random() * 10000);
// Simulate a random success or failure
let success = Math.random() > 0.5;
// Wait for the delay to finish
setTimeout(() => {
// If success, resolve the promise with the flight details
if (success) {
resolve({
airline: "AirBnB",
price: "$199",
duration: "3h 15m",
});
}
// If failure, reject the promise with an error
else {
reject(new Error("No flights available"));
}
}, delay);
});
In our example, the flightPromise
object is a promise that will either be fulfilled with an object containing the flight details, or rejected with an error object, after a random delay. However, creating a promise is not enough. We also need to use it somehow.
Using Promises
To use a promise, we need to attach handlers to it, which are functions that will be executed when the promise is settled. There are two main methods to attach handlers: .then()
and .catch()
.
The .then() Method
The .then()
method takes two arguments: a fulfillment handler, which is a function that will be called if the promise is fulfilled, and an rejection handler, which is a function that will be called if the promise is rejected. The .then()
method returns a new promise, which can be chained with another .then()
method, or a .catch()
method.
For example, the following code uses the flightPromise
object and attaches handlers to it:
// Use the flight promise
flightPromise
// Attach a fulfillment handler
.then((flight) => {
// Log the flight value
console.log(flight);
// Return a new promise that resolves with a hotel
return new Promise((resolve, reject) => {
resolve({
name: "Hotel California",
rating: "⭐⭐⭐⭐",
price: "$99",
});
});
})
// Attach another fulfillment handler
.then((hotel) => {
// Log the hotel value
console.log(hotel);
// Return a confirmation message
return "Your booking is confirmed!";
})
// Attach a rejection handler
.catch((error) => {
// Log the error value
console.error(error);
// Return a cancellation message
return "Your booking is cancelled.";
})
// Attach a final handler
.then((message) => {
// Log the message value
console.log(message);
});
The output of this code will depend on the random success or failure of the flightPromise
object. If it is fulfilled, the output will be something like:
{ airline: 'AirBnB', price: '$199', duration: '3h 15m' }
{ name: 'Hotel California', rating: '⭐⭐⭐⭐', price: '$99' }
Your booking is confirmed!
If it is rejected, the output will be something like:
Error: No flights available
Your booking is cancelled.
Notice that the .then()
method can return different types of values, such as strings, numbers, objects, or even other promises. If the returned value is a promise, the next handler will wait for it to settle, and use its value as the argument. If the returned value is not a promise, the next handler will use it as the argument directly.
The .catch() Method
The .catch()
method takes one argument: a rejection handler, which is a function that will be called if the promise is rejected, or if any error occurs in the previous handlers. The .catch()
method also returns a new promise, which can be chained with another .then()
or .catch()
method.
For example, the following code uses the flightPromise
object and attaches a different handler to it:
// Use the flight promise
flightPromise
// Attach a fulfillment handler
.then((flight) => {
// Log the flight value
console.log(flight);
// Throw an error
throw new Error("Something went wrong");
})
// Attach a rejection handler
.catch((error) => {
// Log the error value
console.error(error);
// Return a cancellation message
return "Your booking is cancelled.";
})
// Attach a final handler
.then((message) => {
// Log the message value
console.log(message);
});
The output of this code will be something like:
{ airline: 'AirBnB', price: '$199', duration: '3h 15m' }
Error: Something went wrong
Your booking is cancelled.
Notice that the .catch()
method can handle both the rejection of the flightPromise
object, and the error thrown by the first handler. The .catch()
method can be used to handle any errors that may occur in the promise chain, and provide a fallback value or action.
Promise Methods
Besides the .then()
and .catch()
methods, there are some other methods that can be used with promises, such as .finally()
, Promise.all()
, Promise.race()
, Promise.resolve()
, and Promise.reject()
.
The .finally() Method
The .finally()
method takes one argument: a final handler, which is a function that will be called after the promise is settled, regardless of its state. The .finally()
method does not affect the value or the state of the promise, but it can be used to perform some cleanup or final actions. For example, the following code uses the .finally()
method to log a message after the flightPromise
object is settled:
// Use the flight promise
flightPromise
// Attach a fulfillment handler
.then((flight) => {
// Log the flight value
console.log(flight);
})
// Attach a rejection handler
.catch((error) => {
// Log the error value
console.error(error);
})
// Attach a final handler
.finally(() => {
// Log a message
console.log("The promise is settled.");
});
The output of this code will be something like:
{ airline: 'AirBnB', price: '$199', duration: '3h 15m' }
The promise is settled.
Or:
Error: No flights available
The promise is settled.
The Promise.all() Method
The Promise.all()
method takes an array of promises as an argument, and returns a new promise that will be fulfilled with an array of values, if all the promises in the array are fulfilled, or rejected with the first reason, if any of the promises in the array are rejected. The Promise.all()
method can be used to wait for multiple promises to finish, and use their values together. For example, the following code uses the Promise.all()
method to book a flight, a hotel, and a car:
// Create a function that returns a promise that simulates a flight search
function searchFlight() {
return new Promise((resolve, reject) => {
// Simulate a random delay
let delay = Math.floor(Math.random() * 10000);
// Simulate a random success or failure
let success = Math.random() > 0.5;
// Wait for the delay to finish
setTimeout(() => {
// If success, resolve the promise with the flight details
if (success) {
resolve({
airline: "AirBnB",
price: "$199",
duration: "3h 15m",
});
}
// If failure, reject the promise with an error
else {
reject(new Error("No flights available"));
}
}, delay);
});
}
// Create a function that returns a promise that simulates a hotel search
function searchHotel() {
return new Promise((resolve, reject) => {
// Simulate a random delay
let delay = Math.floor(Math.random() * 10000);
// Simulate a random success or failure
let success = Math.random() > 0.5;
// Wait for the delay to finish
setTimeout(() => {
// If success, resolve the promise with the hotel details
if (success) {
resolve({
name: "Hotel California",
rating: "⭐⭐⭐⭐",
price: "$99",
});
}
// If failure, reject the promise with an error
else {
reject(new Error("No hotels available"));
}
}, delay);
});
}
// Create a function that returns a promise that simulates a car rental
function rentCar() {
return new Promise((resolve, reject) => {
// Simulate a random delay
let delay = Math.floor(Math.random() * 10000);
// Simulate a random success or failure
let success = Math.random() > 0.5;
// Wait for the delay to finish
setTimeout(() => {
// If success, resolve the promise with the car details
if (success) {
resolve({
model: "Tesla Model 3",
color: "red",
price: "$49",
});
}
// If failure, reject the promise with an error
else {
reject(new Error("No cars available"));
}
}, delay);
});
}
// Use the Promise.all() method to book a flight, a hotel, and a car
Promise.all([searchFlight(), searchHotel(), rentCar()])
// Attach a fulfillment handler
.then((values) => {
// Log the values array
console.log(values);
// Return a confirmation message
return "Your booking is confirmed!";
})
// Attach a rejection handler
.catch((error) => {
// Log the error value
console.error(error);
// Return a cancellation message
return "Your booking is cancelled.";
})
// Attach a final handler
.then((message) => {
// Log the message value
console.log(message);
});
The output of this code will depend on the random success or failure of the promises in the array. If all of them are fulfilled, the output will be something like:
[
{ airline: 'AirBnB', price: '$199', duration: '3h 15m' },
{ name: 'Hotel California', rating: '⭐⭐⭐⭐', price: '$99' },
{ model: 'Tesla Model 3', color: 'red', price: '$49' }
]
Your booking is confirmed!
If any of them are rejected, the output will be something like:
Error: No hotels available
Your booking is cancelled.
Notice that the Promise.all()
method will wait for the longest promise to finish, and use the order of the promises in the array to determine the order of the values in the result array.
The Promise.race() Method
The Promise.race()
method takes an array of promises as an argument, and returns a new promise that will be fulfilled or rejected with the value or the reason of the first promise that settles. The Promise.race()
method can be used to implement a timeout or a cancellation mechanism for promises. For example, the following code uses the Promise.race()
method to cancel the flightPromise
object if it takes too long:
// Create a function that returns a promise that rejects after a given time
function timeout(ms) {
return new Promise((resolve, reject) => {
// Wait for the given time to finish
setTimeout(() => {
// Reject the promise with an error
reject(new Error("Timeout"));
}, ms);
});
}
// Use the Promise.race() method to cancel the flight promise if it takes too long
Promise.race([flightPromise, timeout(5000)])
// Attach a fulfillment handler
.then((flight) => {
// Log the flight value
console.log(flight);
// Return a confirmation message
"Your booking is confirmed!";
})
// Attach a rejection handler
.catch((error) => {
// Log the error value
console.error(error);
// Return a cancellation message
return "Your booking is cancelled.";
})
// Attach a final handler
.then((message) => {
// Log the message value
console.log(message);
});
The output of this code will depend on the random delay of the flightPromise
object. If it is less than 5000 milliseconds, the output will be something like:
{ airline: 'AirBnB', price: '$199', duration: '3h 15m' }
Your booking is confirmed!
If it is more than 5000 milliseconds, the output will be something like:
Error: Timeout
Your booking is cancelled.
Notice that the Promise.race()
method will ignore the other promises in the array, once the first promise settles.
The Promise.resolve() Method
The Promise.resolve()
method takes a value as an argument, and returns a new promise that is fulfilled with that value. The Promise.resolve()
method can be used to create a promise from any value, or to convert a thenable object (an object with a .then()
method) to a promise. For example, the following code uses the Promise.resolve()
method to create a promise from a string:
// Use the Promise.resolve() method to create a promise from a string
Promise.resolve("Hello, world!")
// Attach a fulfillment handler
.then((value) => {
// Log the value
console.log(value);
});
The output of this code will be:
Hello, world!
The Promise.reject() Method
The Promise.reject()
method takes a reason as an argument, and returns a new promise that is rejected with that reason. The Promise.reject()
method can be used to create a promise that is already rejected, or to reject a promise with a custom reason. For example, the following code uses the Promise.reject()
method to create a promise that is rejected with an error:
// Use the Promise.reject() method to create a promise that is rejected with an error
Promise.reject(new Error("Something went wrong"))
// Attach a rejection handler
.catch((error) => {
// Log the error
console.error(error);
});
The output of this code will be:
Error: Something went wrong
Advantages of Promises
Promises have several advantages over callbacks and events for handling asynchronous operations. Some of the main advantages are:
- Promises are easy to read and write, as they use a chainable syntax that avoids the callback hell problem, where nested callbacks make the code hard to follow and maintain.
- Promises are easy to compose and reuse, as they can be returned from functions, passed as arguments, stored in variables, or combined with other promises using methods like
Promise.all()
orPromise.race()
. - Promises are easy to handle errors, as they can use a single
.catch()
method to handle any errors that may occur in the promise chain, instead of using multiple error callbacks or event listeners. - Promises are consistent and predictable, as they follow a well-defined specification that ensures they behave in the same way across different platforms and libraries.
Best Practices for Promises
Promises are powerful and flexible tools for asynchronous programming, but they also require some care and attention to use them effectively and avoid common pitfalls. Some of the best practices for promises are:
- Always return a promise from a
.then()
or.catch()
handler, unless you want to end the chain. This will allow you to chain more handlers, or handle errors properly. - Always handle errors or rejections, either by using a
.catch()
method at the end of the chain, or by using a second argument in the.then()
method. This will prevent unhandled promise rejections, which can cause memory leaks or silent failures. - Avoid creating unnecessary promises, such as wrapping a synchronous value or function in a promise. This will improve the performance and readability of your code.
- Avoid mutating or modifying the values or the state of the promises, such as changing the value of a resolved promise, or calling
resolve
orreject
multiple times. This will ensure the integrity and consistency of your promises. - Use descriptive and meaningful names for your promises and their values, such as
flightPromise
orflight
, instead ofp
orx
. This will make your code more understandable and maintainable.
Conclusion
Promises are a powerful and elegant way to handle asynchronous operations in JavaScript. They allow us to write clean, readable, and modular code that can handle multiple tasks at the same time, without blocking the main thread of execution. Promises also make it easy to handle errors and failures, and to compose and reuse different functions that return promises. By following the promise specification and some best practices, we can ensure that our promises are consistent, predictable, and reliable. Promises are not only a feature of the JavaScript language, but also a paradigm of programming that can help us create better and more efficient applications.