Skip to content
Theory · Asynchronous code · lesson 3 of 12

Promises

A timer's result can be used only inside the callback. You cannot hand it out with return: when the function ends, the result does not exist yet. Let's see:

JavaScript
function loadName() {
    setTimeout(() => {
        return "Ann";
    }, 100);
}

console.log(loadName()); // undefined

return "Ann" leaves the callback, not loadName, and that happens 100 milliseconds later. loadName itself reaches its end at once and without a return, so it returns undefined.

Think of a café where an order is not ready right away. At the counter you get an electronic pager instead of the dish. You can take it to a table and get on with your own things. When the dish is ready, the pager lights up. If the dish has run out, it shows that the order is canceled.

An object for later

A promise is an object for a result that will be ready later. It is that pager: the function returns the promise at once, and the value gets into it afterwards:

JavaScript
function loadName() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("Ann");
            resolve("Bob");
        }, 100);
    });
}

loadName().then((name) => {
    console.log(name);
});
// Ann

new Promise takes a function with two parameters and runs it at once. The parameters receive two functions:

  • calling resolve(value) fulfills the promise with a value;
  • calling reject(reason) rejects it with a reason, usually an Error object.

Three states

A promise has three states:

  • pending: the promise is waiting, the pager is silent;
  • fulfilled: the promise is fulfilled with a value, the dish is ready;
  • rejected: the promise is rejected with a reason, the order is canceled.

At first a promise is pending. Then it becomes fulfilled with a value or rejected with a reason and never changes again.

In the example, the promise stays pending for 100 milliseconds, until the timer fires. The second call, resolve("Bob"), does nothing: the promise is already fulfilled with "Ann".

Here the pager analogy stops working. A café hands its pager to the next guest, but a promise is not reused: it is fulfilled or rejected once and for good.

Interviewers like to ask this: what is a promise, and what are its states? A good answer names all three states and adds that the state changes only once after pending.

then and catch

The then method adds a callback for the value, and catch adds one for the reason of a rejection. They do not stop the program: they only remember the callback, and the lines after them run right away.

JavaScript
function checkAge(age) {
    return new Promise((resolve, reject) => {
        if (age >= 0) {
            resolve(age);
        } else {
            reject(new Error("bad age"));
        }
    });
}

checkAge(-1)
    .then((age) => age + 1)
    .then((next) => console.log(next))
    .catch((e) => console.log(e.message));
// bad age

A call chain, as in the lesson on map and filter, can be split across lines: a line that starts with a dot continues the one above. The four lines from checkAge(-1) to catch are one expression.

Each then returns a new promise holding the value its callback returned. That way the steps line up into a chain: the next then gets what the previous one returned.

Let's go step by step through what happens with checkAge(-1):

  1. The number -1 is below zero, so checkAge rejects the promise with the error bad age.
  2. The first then skips its callback and passes the rejection on down the chain.
  3. The second then skips its callback too.
  4. catch receives the error and prints bad age.

With checkAge(20) it goes differently: the promise is fulfilled with 20, the first then returns 21, the second prints 21, and catch is not called. One catch at the end is enough for the whole chain.

The reason of the rejection reaches the catch callback as the parameter e, as in try…catch, and e.message gives its text. Node.js treats a rejection that nobody catches as an error and stops the program. A browser writes it to the console.

Common mistake

A promise is used as if it were the ready value:

JavaScript
function loadName() {
    return new Promise((resolve) => {
        resolve("ann");
    });
}

const name = loadName();
console.log(name.toUpperCase()); // ✗
TEXT
TypeError: name.toUpperCase is not a function

name holds a promise, not a string, and a promise has no toUpperCase method. It is like trying to eat the pager. The value is taken in a callback: loadName().then((name) => console.log(name.toUpperCase())) prints ANN.

Key points

  • A promise is an object for a result that will be ready later; a function returns it at once.
  • A promise is pending until it becomes fulfilled with a value or rejected with a reason, and then it never changes.
  • then and catch only add callbacks, and each then returns a new promise for the next step of the chain.
  • A rejection skips every then up to the nearest catch.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way