What is a Promise? JavaScript Promises for Beginners
If you‘re new to JavaScript, you may have heard the term "promise" thrown around, but had trouble understanding exactly what it means. Promises can seem complex and confusing at first, but they don‘t have to be. In this beginner‘s guide, I‘ll break down JavaScript promises in simple terms and show you how to start using them in your code.
What are Promises?
A promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. In other words, a promise is an object that may return a single value sometime in the future – either a resolved value or a reason why it was not resolved (rejected). This lets asynchronous operations like API calls, file I/O, database queries, etc. return values like synchronous functions, rather than immediately returning the final value.
Promises are an alternative to using callbacks for asynchronous code. With callbacks, you typically pass a function as an argument to another function which performs an async operation. The callback is then executed once the operation completes. While this works, it can lead to confusing and hard to maintain "callback hell" if you have callback functions nested inside each other.
Promises, on the other hand, allow you to chain async operations together and handle errors in a more readable and maintainable way. Let‘s look at an analogy to better understand the concepts.
Promises Explained: Ordering Food at a Restaurant
Imagine you go to a restaurant and order a burger. When you place your order, the cashier gives you an order number and you go find a table to sit at. Your burger isn‘t ready immediately, so the cashier is promising to deliver your food as soon as it‘s ready. This is similar to an asynchronous operation that will complete sometime in the future.
While waiting for your food, your order can be in one of three possible states:
-
Pending – The initial state before the burger is ready. The promise‘s outcome hasn‘t been determined yet because the operation hasn‘t completed.
-
Fulfilled – Your burger is ready and the cashier brings it to your table. The promise has successfully resolved and you can now eat your food. This represents a successful async operation.
-
Rejected – Something went wrong in the kitchen and the chef burnt your burger. The cashier apologizes and lets you know they can‘t fulfill your order. This represents an async operation that failed and could not be resolved.
Every promise starts in the pending state and will transition to either the fulfilled or rejected state, depending if the operation it represents was successful or not. Once a promise reaches the fulfilled or rejected state, it is settled and its state will not change again.
Creating and Using Promises in JavaScript
Now that you understand the basic concepts, let‘s see how promises work in JavaScript code. A promise is created using the new
keyword and passing a function (called the "executor") that takes two arguments – resolve
and reject
:
const myPromise = new Promise((resolve, reject) => {
// Async operation here
// Call resolve(value) when fulfilled
// Call reject(error) when rejected
});
The executor function runs automatically when the promise is constructed. Inside it, you perform your async operation and call resolve(value)
when it succeeds to fulfill the promise with a value. If an error occurs, you call reject(error)
instead to reject the promise with an error reason.
Here‘s an example that creates a promise to simulate flipping a coin:
const flipCoin = new Promise((resolve, reject) => {
const side = Math.random() < 0.5 ? ‘heads‘ : ‘tails‘;
if(side === ‘heads‘) {
resolve(side); // Fulfilled
} else {
reject(new Error(‘Oops, it landed on tails!‘)); // Rejected
}
});
To use a promise after creating it, you call its then()
method and pass the function(s) you want executed when the promise is fulfilled or rejected:
flipCoin.then(
(value) => console.log(`Yay, it landed on ${value}!`),
(error) => console.error(error)
);
then()
takes two arguments – a fulfillment handler and a rejection handler. If the promise fulfills, the fulfillment handler is called with the fulfilled value. If the promise rejects, the rejection handler is called with the error reason. You can omit the rejection handler and instead add a catch()
block at the end:
flipCoin
.then((value) => console.log(`Yay, it landed on ${value}!`))
.catch((error) => console.error(error));
This uses catch()
to handle any errors and rejections that may happen in the promise chain. It‘s a good practice to always include error handling with catch()
.
Chaining Promises
Promises really shine when you need to perform multiple async operations in sequence. Instead of nesting callbacks, you can chain promises together and keep the code flat:
asyncOperation1()
.then((result1) => {
console.log(result1);
return asyncOperation2();
})
.then((result2) => {
console.log(result2);
return asyncOperation3();
})
.then((result3) => {
console.log(result3);
})
.catch((error) => {
console.error(error);
});
Each then()
block receives the value returned from the previous one, so you can pass data between the async operations. Returning a value from then()
will pass it to the next then()
block in the chain. Throwing an error at any point will skip to the catch()
block.
Here‘s a real-world example using the Fetch API to make HTTP requests:
fetch(‘https://api.example.com/data‘)
.then((response) => response.json())
.then((data) => {
console.log(data);
// Modify data and send to another endpoint
data.foo = ‘bar‘;
return fetch(‘https://api.example.com/modify‘, {
method: ‘POST‘,
body: JSON.stringify(data)
});
})
.then((response) => response.json())
.then((modifiedData) => {
console.log(modifiedData);
})
.catch((error) => {
console.error(error);
});
This fetches JSON data from an API endpoint, modifies the data, sends it to another endpoint, and logs the response, handling any errors along the way. Promises make this kind of sequential async flow much easier to follow.
Why Use Promises?
In addition to avoiding callback hell and improving readability, promises provide several other benefits:
Error handling – Promises make it easy to handle errors at any point in the async operation chain without using complex nested try/catch blocks. Any errors or rejections are caught by the catch()
block at the end of the chain.
Parallel execution – You can perform async operations in parallel and wait until all of them are fulfilled before continuing by using Promise.all()
. This lets you maximize efficiency.
Chaining and piping – As shown above, you can chain multiple async operations together, piping the output of one operation as the input to the next for complex flows and data manipulation.
Browser support – Promises have been supported in all modern browsers for several years, so you can reliably use them without a polyfill.
Conclusion
I hope this article has helped clarify what JavaScript promises are and how to use them. To recap, remember that:
- A promise represents the eventual completion or failure of an async operation
- Promises can be in one of three states – pending, fulfilled, or rejected
- Create a promise with the
new Promise()
constructor and perform async operations in the executor - Use
resolve(value)
to fulfill the promise when the async operation succeeds - Use
reject(error)
to reject the promise if an error occurs - Handle fulfilled promises with
then()
and rejected promises withcatch()
- Chain promises together to perform a series of async operations in sequence
There‘s a lot more that can be done with promises, and I encourage you to explore further. Some additional promise-related topics to look into include:
Promise.all()
andPromise.race()
async/await
syntax for working with promises- Promisifying callback-based functions
- Cancelling promises with AbortController
Promises are a powerful tool for any JavaScript developer. I hope this guide has given you the foundation to start using them effectively in your code. Now go out there and make some promises! (And remember to always handle your rejections ?)