JavaScript Promises Explained
What are Promises?
Try to imagine you are doing some online shopping.
You select a nice new pair of shoes and add them to your cart. Then you slap in your card details and complete the order.
You don't get your shoes instantly. Instead, you enter into an agreement where the store will fulfill and deliver your order soon, or to make it more relevant, in our case, the store "promises" to deliver our shoes and update us if there are any issues.
Just like we don't stop everything to wait for our shoes (or at least I hope you don't), a Promise allows you to handle asynchronous operations more flexibly.
A Promise is an object representing an asynchronous operation's eventual completion or failure.
After we create and execute a promise, there are three states our promise can be in:
"pending"
"fulfilled"
"rejected"
Using this simplified analogy, let's look into what states a Promise can be in:
- Create a Promise: When we order the new shoes. When we are waiting.
- Pending: While we are waiting for our shoes to arrive.
- Fufilled: When the order arrives at our door.
- Rejected: Maybe the store was out of stock and had to cancel your order.
Just like waiting for your order, JavaScript often has to wait for things like data to load before running the next piece of code. Promises are JavaScript's way of handling these waiting periods.
Now that we have a bit of a mental model, let's look at how to use Promises in JavaScript.
Creating and using Promises
Creating a Promise
A Promise is created using the Promise constructor function. This function takes one argument, a callback function that takes two parameters: resolve
and reject
.
const newOrder = new Promise((resolve, reject) => { // Some asynchronous operation });
Both resolve
and reject
are functions to handle the success and failure of our actions. We will see how to use them in the next sections.
Fulfilling a Promise
If the asynchronous operation is successful, you will call the resolve function with the resulting value.
const newOrder = new Promise((resolve, reject) => { // Function is executed when the promise is constructed // after 1 second resolve with value `{ orderedShoes: true }` setTimeout(() => resolve({ orderedShoes: true }), 1000); });
The resolved value can be anything you like! Object, string, number, or whatever valid JS you can dream up.
Rejecting a Promise
If the asynchronous operation failed, you would call the reject function with an Error object.
const newOrder = new Promise((resolve, reject) => { reject(new Error("Something went wrong!")); // The Promise is now rejected with the given error });
Note: The Error
object isn't mandatory but recommended. You can pass any value to the reject function, but not returning an Error
is considered "bad practice".
Resolve or Reject
We can only use a single resolve
or reject
before the function ends its execution. Any code in our function will be ignored after calling resolve resolve
or reject
.
You'll usually structure code with a try/catch
or if/else
to decide which function should be called during execution.
Use the reject
to handle the cases where the primary action was a failure.
Using the Result of a Promise
The result of a Promise can be used by calling its then
method, which registers two callbacks to receive either the Promise's eventual value or the reason why the Promise cannot be fulfilled.
// promise.then(resultHandler, errorHandler); promise.then( result => { console.log(result); }, error => { console.error(error); } );
So if we were using our older example, this is how we would call it and use the result:
const newOrder = new Promise((resolve, reject) => { setTimeout(() => resolve({ orderedShoes: true }), 1000); }); newOrder.then(result => console.log(result)); // Outputs: `{ orderedShoes: true }`
As you can see in this example, we didn't pass the second parameter for an error here.
And it's because we only cared about a successful result.
We will look at a second way to handle errors next.
Catch
I usually call the catch
method to handle errors at the end of a promise chain. I find it easier to read (it is a personal preference and common practice).
In this example, we are only handling errors and don't care about the success result:
const newOrder = new Promise((resolve, reject) => { reject(new Error("Something went wrong!")); // The Promise is now rejected with the given error }); // Catch is like writing `then(undefined, reason => {})` newOrder.catch(errorReason => console.error(errorReason));
But we can also use it after a .then
to handle a failure.
promise .then(result => { console.log(result) }) .catch(error => { console.error(error); })
I think this option looks a little more verbose than passing two functions as handlers inside the .then
.
But where I think it shines is when we start chaining promises and we can catch errors in a single place for a list of Promises.
We will keep Promise chaining for another article to avoid making this topic too overwhelming. 🙈
Follow me on Twitter or connect on LinkedIn.
🚨 Want to make friends and learn from peers? You can join our free web developer community here. 🎉