Bitsy Explains
Home/JavaScript, Explained Like You're Five/Lesson 18

Promises in JavaScript

A promise is a stand-in for a value that arrives later, like a restaurant buzzer. Learn its three states, .then and .catch, and how promises flatten callback hell.

Lesson 18 of 203 min readVideo
Video thumbnail: Promises Watch the 90-second video
Explain like I'm five

You order food and get a buzzer. Right now you're waiting, and that's pending. The buzzer goes off and your food is ready, which is fulfilled. Or the kitchen burned it, and that's rejected. You can't have the food yet, but you can hold the buzzer right now.

Three states

A promise is like that buzzer. It stands in for a value that's coming later. Every promise is in one of three states.

  • Pending: still waiting. No value yet.
  • Fulfilled: it worked, and the value is ready.
  • Rejected: something went wrong, and there is an error instead.

A promise starts out pending and then settles once, either fulfilled or rejected. After that it never changes. A buzzer doesn't go off twice.

The buzzer idea is the whole point. The kitchen can't give you the food yet, but it can give you something to hold right now. A function that does slow work can return a promise straight away, and you can pass that promise around, store it, or attach code to it before the value even exists.

.then and .catch

.then runs your function when the promise is fulfilled. It gets the value.

const buzzer = Promise.resolve("pizza");
buzzer.then(food => console.log("Eating " + food)); // prints: Eating pizza

.catch runs your function when the promise is rejected. It gets the error.

const buzzer = Promise.reject(new Error("burned"));
buzzer.catch(err => console.log("Problem: " + err.message)); // prints: Problem: burned

Promise.resolve and Promise.reject make promises that are already settled, which is handy for trying things out. In real code the promise usually takes a while to settle. Your .then or .catch still runs later, never in the middle of the code that set it up.

Make your own

You can make your own promise with new Promise. You give it a function that receives two other functions. Call resolve to fulfill the promise when things go well, and reject when they don't.

const order = new Promise((resolve, reject) => {
  setTimeout(() => resolve("pizza"), 500);
});
order.then(food => console.log("Got " + food)); // prints: Got pizza
console.log("Still waiting"); // prints: Still waiting

"Still waiting" prints first. Half a second later the promise is fulfilled and "Got pizza" prints. If the kitchen failed, you would call reject(new Error("burned")) instead.

Most of the time, though, you'll use promises that other code gives you. For example, fetch in the browser gets data from the internet and returns a promise.

The twist: no more callback hell

Remember callbacks nested inside callbacks? Promises flatten them. Each step goes in its own .then, in one straight line from top to bottom.

const getUser = id => Promise.resolve({ id, name: "Mia" });
const getOrders = user => Promise.resolve(["pizza", "juice"]);
const countItems = orders => orders.length;
getUser(1)
  .then(getOrders)
  .then(countItems)
  .then(n => console.log(n + " items")); // prints: 2 items

Each .then gets whatever the step before it returned. If a step returns a promise, the chain waits for it before moving on. And one .catch at the end handles an error from any step, so you don't repeat error handling at every level.

There is also .finally, which runs either way, fulfilled or rejected. It's a good place for cleanup, like hiding a loading spinner.

Rule of thumb

Use .then for success and .catch for errors.

Common mistake

Forgetting to return inside a .then that uses curly braces. .then(user => { getOrders(user); }) passes undefined to the next step and doesn't wait. Write return getOrders(user); or drop the braces.

Check yourself

A promise was rejected. Which part of p.then(a).catch(b) runs?