RizTech Academy logo
RizTech Academy
Asynchronous JavaScriptLesson 3 of 730 min

Promises

A promise is an object standing in for a value that has not arrived yet. That one idea fixes every complaint from the last lesson: sequence becomes a list rather than a staircase, errors are handled once, and try/catch starts working again.

What it is

const orderPromise = getOrder(1);

console.log(orderPromise);
Promise { <pending> }

Not the order — a receipt for the order. It is in one of three states:

State Means
pending Still waiting
fulfilled Done, with a value
rejected Failed, with a reason

A promise settles once and never changes again. That is the guarantee callbacks could not give: it cannot fulfil twice, and it cannot fulfil and then reject.

Using one

getOrder(1)
  .then((order) => {
    console.log(order.customer);
  })
  .catch((error) => {
    console.log('Failed:', error.message);
  });

.then runs on success with the value; .catch runs on failure with the reason. Compare with the error-first callback — the two paths are now separate, and the success path contains no error checking at all.

.finally runs either way, and is where the loading spinner gets turned off:

getOrder(1)
  .then(showOrder)
  .catch(showError)
  .finally(() => {
    spinner.hidden = true;
  });

Chaining, which is the point

.then returns a new promise, so they queue up:

Promise.resolve(2)
  .then((n) => n * 10)
  .then((n) => n + 1)
  .then((n) => console.log(n));
21

Each .then receives what the previous one returned. That is the pyramid, flattened into a list:

getCustomer(1)
  .then((customer) => getOrder(customer.id))
  .then((order) => getSlot(order.id))
  .then((slot) => console.log(slot.time))
  .catch(showError);

Four steps, no nesting, and one .catch for all of them — a rejection anywhere skips every remaining .then and lands there. Compare the three identical error blocks from the last lesson.

Returning a promise from a .then flattens it. getOrder(customer.id) returns a promise, and the next .then receives the order rather than the promise. Without that, chaining would nest as badly as callbacks.

The trap: the missing return

Promise.resolve(2)
  .then((n) => {
    n * 10;
  })
  .then((n) => console.log(n));
undefined

Braces make a function body, so nothing is returned, so the next .then gets undefined — module 3's arrow-function trap, in the place it costs most.

It is worse with an asynchronous step:

getCustomer(1)
  .then((customer) => {
    getOrder(customer.id);
  })
  .then((order) => console.log(order));
undefined

The order is fetched — but nothing was returned, so the chain did not wait for it and moved on immediately. A .then that starts async work without returning it breaks the sequence silently, and any error inside it escapes your .catch entirely.

Rule: every .then either returns a value or returns a promise.

Recovering

.catch also returns a promise, so a chain continues after it:

Promise.reject(new Error('x'))
  .catch(() => 'recovered')
  .then((value) => console.log(value + '!'));
recovered!

Useful for a default when a request fails. It also means a .catch in the middle of a chain handles only what came before it — put it last unless you specifically want to recover and carry on.

Making one

Usually you do not; fetch and other APIs hand you promises already. You write one to wrap a callback-style API:

function wait(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

wait(1000).then(() => console.log('one second later'));

new Promise takes a function with two arguments: call resolve(value) to fulfil, reject(error) to fail.

Wrapping an error-first callback is the common case:

function getOrder(id) {
  return new Promise((resolve, reject) => {
    getOrderCallback(id, (error, order) => {
      if (error) {
        reject(error);
        return;
      }
      resolve(order);
    });
  });
}

Written once, and the whole API is modern.

Always reject with an Error, not a string — module 5's rule. You want a stack trace.

Two shortcuts: Promise.resolve(value) and Promise.reject(error) make already-settled promises, useful for tests and for a cached value.

Several at once

This is where promises stop being tidier callbacks and start being genuinely more capable. The counter-and-flag bookkeeping from the last lesson becomes:

const orders = await Promise.all([getOrder(1), getOrder(2), getOrder(3)]);
Combinator Resolves when Rejects when
Promise.all All succeed — array of results, in order Any fails, immediately
Promise.allSettled All settle — never rejects Never
Promise.race The first settles, either way If the first settles as a rejection
Promise.any The first success Only if all fail

Promise.all gives results in the order you passed them in, regardless of which finished first — the index bookkeeping, done for you.

Promise.all fails fast, and that is often wrong. Loading three independent panels, one failing should not blank all three:

const results = await Promise.allSettled([getOrders(), getMenu(), getSlots()]);

for (const result of results) {
  if (result.status === 'fulfilled') {
    render(result.value);
  } else {
    showError(result.reason);
  }
}
fulfilled,rejected

allSettled gives { status, value } or { status, reason } for each, and never rejects. Use all when you need everything and allSettled when you want as much as you can get.

race is for timeouts:

Promise.race([
  getOrder(1),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Too slow')), 5000),
  ),
]);

fetch has no timeout of its own, and on a train that matters.

They start immediately

A common misunderstanding:

const promise = getOrder(1);

The request is already in flight. A promise is not a recipe you run later — the work started when you called the function. .then only decides what happens with the result.

That is why Promise.all([a(), b()]) runs both at once: both were called, and therefore both started, before all ever saw them.

Check your work

A promise is a receipt for a future value, in one of three states, and it settles once and stays settled.

.then returns a new promise, so chains flatten the callback pyramid into a list, and one .catch at the end handles a rejection from any step.

Returning a promise from .then flattens it, so the next step gets the value.

A .then with braces and no return passes undefined on. If it started async work without returning it, the chain does not wait and errors inside escape your .catch.

.catch returns a promise too, so .catch(() => 'recovered').then(...) continues with the recovery value.

new Promise((resolve, reject) => ...) wraps a callback API. Reject with an Error.

Promise.all returns results in argument order and fails fast; allSettled never rejects and reports { status, value } or { status, reason } per entry. race settles on the first to finish; any waits for the first success.

Promises start immediately. The work began when the function was called, not when .then was attached.

Practice

  1. Log a promise before it settles and confirm you see Promise { <pending> }.
  2. Write wait(ms) returning a promise and use it with .then.
  3. Chain three .thens doing arithmetic and predict the output.
  4. Write the missing-return bug. Use braces in a .then without return and watch undefined arrive downstream. Fix it twice — by removing the braces and by adding return.
  5. Make the async version of that bug: call another promise-returning function inside a .then without returning it, and confirm the chain does not wait.
  6. Build a three-step chain with one .catch at the end. Make the middle step reject and confirm the third step is skipped.
  7. Put a .catch in the middle instead, returning a default, and confirm the chain continues.
  8. Add .finally and confirm it runs on both paths.
  9. Wrap an error-first callback function in a promise and use it with .then/.catch.
  10. Use Promise.all on three waits of different lengths. Time it, and confirm the results come back in argument order rather than finishing order.
  11. Make one of them reject and watch all fail immediately. Then switch to allSettled and handle each result.
  12. Harder. Write withTimeout(promise, ms) using Promise.race that rejects with a clear Error if the promise takes too long. Test it against a fast and a slow operation. Then answer, in a comment: when the timeout wins, what happens to the original request?

Next: async/await — the same promises, written as if they were synchronous.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship