What is a Promise?
In everyday language, a promise is a declaration or assurance that one will do a particular thing or that a particular thing will happen, and it can either be kept or broken. In JavaScript, a Promise is similar: it's simply an object with a value that will either be fulfilled or rejected. If a Promise is fulfilled, the application will do this; if the Promise is rejected, the application will do that.
JavaScript Promises are most commonly used when handling asynchronous operations. It's important to have a strong understanding of synchronous and asynchronous programming before working with Promises. If you don't understand asynchronous JavaScript, I have an article which explains it in full detail here:
How are Promises used?
Let's say that you need to make an API request in order to fetch some data. Retrieving data is something unpredictable and out of your control — it can be fast, slow, successful, or even fail completely. Because of this, API requests must be handled asynchronously, typically through a Promise. Promises ensure that the correct course of action is taken, regardless of whether the request is successful or not.
How do you create a Promise?
Generally, an instance of a Promise is created by using the new
keyword followed
by the Promise()
constructor function. This function accepts a function as its
argument, which looks like this:
const promise = new Promise(() => {
// ...
});
...this function takes in two arguments, resolve
and
reject
, which are also functions...
const promise = new Promise((resolve, reject) => {
// ...
});
...one thing to note is that a Promise has 3 different states...
- Pending: Initial state, neither fulfilled nor rejected.
- Fulfilled: The operation was completed successfully.
- Rejected: The operation failed.
...with this in mind, resolve()
is a function that when called, changes the state of the Promise
from pending to fulfilled. On the other hand, reject()
is a function that
when called, changes the state of a Promise from pending to rejected.
As stated earlier, Promises are generally used when working with data, but for simplicity's sake, I'll use the following example:
const promise = new Promise((resolve, reject) => {
let age = 17;
if (age >= 21) {
resolve();
} else {
reject();
}
});
...here's a Promise that uses a conditional statement. If age
is 21 or greater, the
Promise will be fulfilled; if age
is below 21, the Promise will be rejected. If the Promise is
fulfilled, I want old enough to drink
logged to the console; if the Promise is
rejected, I want too young to drink
logged to the console. In order to do this, I'll create two
separate callback functions — one for handling the fulfilled state and another for handling the rejected state...
// ...
const oldEnoughMessage = () => {
console.log("old enough to drink");
};
const tooYoungMessage = () => {
console.log("too young to drink");
};
// ...
...now these functions can be combined with the Promise, but how?
.then() and .catch() methods
oldEnoughMessage
and tooYoungMessage
are both
callback functions, which means they are passed in as arguments to other functions. Those other functions
are the .then()
and .catch()
methods,
which the Promise
constructor function gives us access to.
Remember when I said that a Promise's course of action is determined by its success or failure? Well, those actions
lie in the .then()
and .catch()
methods. If the
Promise is fulfilled (age
is equal to or greater than 21),
the .then()
method/function will be executed. If the Promise is rejected
(age
is less than 21), the .catch()
method/function will be executed.
With this in mind, we can pass in each callback function accordingly, like so:
const promise = new Promise((resolve, reject) => {
let age = 17;
if (age >= 21) {
resolve();
} else {
reject();
}
});
const oldEnoughMessage = () => {
console.log("old enough to drink");
};
const tooYoungMessage = () => {
console.log("too young to drink");
};
promise.then(oldEnoughMessage);
promise.catch(tooYoungMessage);
// too young to drink
...alteratively, the exact same result can be accomplished by passing in each callback function as an arrow function...
const promise = new Promise((resolve, reject) => {
let age = 17;
if (age >= 21) {
resolve();
} else {
reject();
}
});
promise.then(() => {
console.log(result);
console.log("old enough to drink");
});
promise.catch(() => {
console.log(error);
console.log("too young to drink");
});
// too young to drink
...the latter is the most common way of doing it. If you take a look at the code, you'll notice that the
resolve()
and reject()
functions are still empty.
In order to make use of the value, I'll pass in a string to each one...
// ...
if (age >= 21) {
resolve('Success!');
} else {
reject('Error!');
}
// ...
...to access these strings, just pass them as arguments to the callback functions...
const promise = new Promise((resolve, reject) => {
let age = 17;
if (age >= 21) {
resolve("Success!");
} else {
reject("Error!");
}
});
promise.then((result) => {
console.log(result);
console.log("old enough to drink");
});
promise.catch((error) => {
console.log(error);
console.log("too young to drink");
});
// Error!
// too young to drink
...this works perfectly, but the code can still be cleaned up even further.
Promise chaining
.then()
and .catch()
both return Promises,
so they can be chained together, which is referred to as "Promise chaining". Promise chaining allows
you to chain together multiple .then()
calls, which will run when the previous
Promise is fulfilled. The .catch()
method can still be called to handle any
errors along the way. Promise chaining usually looks something like this:
const promise = new Promise((resolve, reject) => {
let age = 22;
if (age >= 21) {
resolve("Success!");
} else {
reject("Error!");
}
});
promise
.then((result) => {
console.log(result);
console.log("old enough to drink");
const msg = "🍻";
return msg;
})
.then((drinks) => {
console.log(drinks);
})
.catch((error) => {
console.log(error);
console.log("too young to drink");
});
// Success!
// old enough to drink
// 🍻
...in this example, age
is 22, which means the
Promise will be fulfilled, therefore running the first .then()
callback function,
which logs Success!
and old enough to drink
to the console. This callback function also returns a variable called msg
,
which is then passed as the drinks
argument to the chained call, where it finally
gets logged to the console. If age
was less than 21,
only the .catch()
callback function would be invoked.
That's JavaScript Promises in a nutshell. Although there's nothing wrong with using .then()
and .catch()
for handling asynchronous code, ES7 introduces the
async
and await
keywords, which have an even cleaner
syntax, all while offering the same functionality. In my next article, I'll be covering how to use
async/await
to enable asynchronous, Promise-based behavior, as well as comparing it to
.then()/.catch()
, so stay tuned!