Learn JavaScript

Lesson 1 of 9 · Async 2: Promises, async/await and fetch

Module 13 · Async 2: Promises, async/await and fetch

Promises, the Picture: A Token for a Value That Comes Later

FreeReading

In this lesson

  • Explain why a function cannot simply return a value that arrives later, and what a promise gives back instead.
  • Name the three states of a promise and say why a settled promise never changes again.
  • Read a value out of a promise with then, handle a failure with catch, and wait for one with await.

Lima orders kacchi at a busy shop in Old Dhaka. The cook does not hand her a plate on the spot. The man at the counter gives her a token with a number on it. The token is not food. It is a promise that food will come, and it tells her where to stand.

Lima can do other things while she holds the token. She can buy a drink or call a friend. When the number is called, she swaps the token for the plate. If the kitchen runs out of rice, the token turns into an apology instead.

JavaScript has exactly this token. It is called a promise. In Module 12 you met the event loop and callbacks, and you saw callbacks nest into a pyramid. This module is the way out that Module 12 promised. You will learn the token first, then every method on it, then async and await, which make waiting read like plain code.

A value that is not there yet

Here is the problem a promise solves. A function asks for something slow: a price from a server, a file from a disk. It wants to return the answer. But the answer arrives after the function has already finished.

function getPrice() {
  let price;
  setTimeout(() => {
    price = 120;
  }, 10);
  return price;
}

console.log(getPrice());
console.log("the function has already returned");
undefined
the function has already returned
Run in Compiler

The function returns at once, before the timer has fired. So price is still empty when return price runs, and the program prints undefined. The 120 does arrive, ten milliseconds later, but nobody is listening any more.

Module 12's answer was a callback: pass in a function, and call it when the value arrives. That works for one step. With three slow steps in a row, each callback sits inside the one before it. A promise fixes this from the other side. The function returns something right away: a token for the value.

A promise is a token for a later value

A promise is an object that stands for a value that may not exist yet. You make one with new Promise. You hand it a function, called the executor, and JavaScript calls that function at once with two buttons: resolve and reject.

const token = new Promise((resolve) => {
  setTimeout(() => resolve("kacchi"), 10);
});

console.log(token);
token.then((dish) => {
  console.log("got " + dish);
  console.log(token);
});
Promise { <pending> }
got kacchi
Promise { 'kacchi' }
Run in Compiler

The first console.log(token) shows the promise before the timer fires. Node prints it as Promise { <pending> }: the token exists, the food does not. When the timer calls resolve("kacchi"), the promise fills. The then callback runs, and the second print shows the value inside.

Pressing resolve is how the slow code says "here is the value". Pressing reject says "it failed, and here is why". The executor decides which one to press, and when.

Three states, settled once

A promise is always in one of three states:

  • pending: the value is not there yet. Every promise starts here.
  • fulfilled: resolve was called, and the promise now holds its value.
  • rejected: reject was called, and the promise now holds a reason, usually an Error.

Fulfilled and rejected together are called settled. The rule that makes promises safe is short: a promise settles once. After that, every later resolve or reject is ignored.

const p = new Promise((resolve, reject) => {
  resolve("first");
  resolve("second");
  reject(new Error("too late"));
});

p.then((value) => console.log("value:", value));
p.then((value) => console.log("again:", value));
value: first
again: first
Run in Compiler

Only "first" counts. The second resolve and the reject change nothing. And two separate then calls both get the same value, because the promise keeps it. A token, once swapped for a plate, stays a plate. This is what callbacks could not promise: with a callback, a buggy library could call you twice. Module 12's bug gallery showed that.

then: run this when the value arrives

then is how you say "when this promise fills, run this function with the value". The function you pass is a callback, but it is attached to the token, not passed into the slow code. Also then returns a new promise, so you can chain the next step onto it.

const cook = (dish, ms) => new Promise((resolve) => setTimeout(() => resolve(dish), ms));

cook("kacchi", 10)
  .then((dish) => dish + " with borhani")
  .then((meal) => meal.toUpperCase())
  .then((text) => console.log(text));

console.log("the counter keeps serving");
the counter keeps serving
KACCHI WITH BORHANI
Run in Compiler

Each then receives what the step before it returned. The first adds a drink, the second shouts it, the third prints it. Notice the order of the two lines in the output. The last line of the file prints first, because the chain only runs after the timer, on a later turn of the loop. The file did not stop and wait.

Compare this with Module 12's pyramid. Three steps are three flat lines, one under the other, not three levels of nesting.

catch: run this if it fails

When a promise is rejected, the then callbacks are skipped. The rejection falls down the chain until it meets a catch. A catch is a then for the failure path only.

const cook = (dish, ms) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      if (dish === "kacchi") reject(new Error("rice is finished"));
      else resolve(dish);
    }, ms);
  });

cook("kacchi", 10)
  .then((dish) => console.log("eating " + dish))
  .catch((err) => console.log("sorry: " + err.message))
  .then(() => console.log("Lima goes home"));
sorry: rice is finished
Lima goes home
Run in Compiler

The then that would eat is skipped, the catch runs with the error, and the chain carries on after it. A catch that returns normally turns the chain back into a fulfilled one. That is why the last line still runs. Lesson 02 shows every rule of this flow.

await: wait inside an async function

Chains are flat, but they still read sideways. JavaScript has a second spelling for the same thing. Inside a function marked async, you can write await before a promise. The function pauses at that line until the promise settles. Then it carries on with the value, as if the value had been there all along.

The pause is the important part. Only this one function pauses. The rest of the program keeps running, and the loop keeps taking turns. When the promise settles, the function picks up exactly where it stopped.

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function countdown() {
  for (let n = 3; n >= 1; n--) {
    console.log(n);
    await sleep(10);
  }
  console.log("go");
}

countdown();
console.log("countdown started");
3
countdown started
2
1
go
Run in Compiler

sleep is the smallest useful promise: it fills after ms milliseconds, with no value. Inside countdown the loop reads top to bottom, with no callback in sight. But look at where "countdown started" lands. The function ran up to its first await, paused, and handed control back to the caller. The caller's next line ran while the countdown slept.

Watch one small chain in the widget below, one step at a time. The promises are on one side with their states, the functions paused at an await on another, and the printed lines below.

The same steps as a table, one row per step.

StepLineWaitingOutputWhat happened
1const order = cook("khichuri", 30);order is pending: the timer runs and the dish is not ready.
2const packed = order.then((dish) => cook(dish + " in a box", 20));then gives back a new promise, packed, which is pending too.
3rider();riderrider runs up to await packed, then pauses there.
4log("next customer");ridernext customerThe file goes on while rider waits; packed is still pending.
5// a later turn of the loopridernext customerorder is fulfilled with 'khichuri'.
6// a later turn of the loopridernext customerpacked is fulfilled with 'khichuri in a box'.
7log("deliver " + parcel);next customer deliver khichuri in a boxrider resumes after its await and prints its line.

Step 3 is the one to remember: rider is paused, yet step 4 still prints. Steps 5 and 6 happen on later turns, when the timers fire. Only after packed fills does rider wake up and print.

Promises at a glance

const p = new Promise((resolve, reject) => { ... });  // the executor runs at once
resolve(value)        reject(new Error("why"))      // settle it, once
p.then((value) => next)                             // runs when p is fulfilled; returns a new promise
p.catch((err) => fallback)                          // runs when p is rejected
async function f() {                                // f always returns a promise
  const value = await p;                            // pause f until p settles
}
Example 1: Module 12's pyramid, flattened

In Module 12, three steps that each waited took three levels of callbacks. Here the same three steps are promises. Each step is a function that returns a promise, and the chain lists them in order.

const step = (name, ms) =>
  new Promise((resolve) => setTimeout(() => resolve(name), ms));

step("wash rice", 10)
  .then((done) => {
    console.log(done);
    return step("cook rice", 20);
  })
  .then((done) => {
    console.log(done);
    return step("add mutton", 10);
  })
  .then((done) => console.log(done + ", ready"));
wash rice
cook rice
add mutton, ready

Each callback returns the next promise, and the chain waits for it before the next then runs. That return is the whole trick. Without it, the next then would run at once with undefined. Lesson 06's bug gallery shows that bug.

Run in Compiler
Example 2: the same steps with await

Here are the same three steps written with async and await. The function reads like a recipe, top to bottom.

const step = (name, ms) =>
  new Promise((resolve) => setTimeout(() => resolve(name), ms));

async function cookKacchi() {
  console.log(await step("wash rice", 10));
  console.log(await step("cook rice", 20));
  const last = await step("add mutton", 10);
  return last + ", ready";
}

cookKacchi().then((text) => console.log(text));
console.log("the kitchen is busy");
the kitchen is busy
wash rice
cook rice
add mutton, ready

The return inside an async function fills the promise that the function returns. That is why the caller reads the final line with then. Every async function returns a promise, always. Lesson 02 shows what happens when one throws.

Run in Compiler

Where this is used

  • Node.js: the fs/promises module gives promise versions of file functions, so await readFile("a.txt", "utf8") reads a file without a callback.
  • Web browsers: fetch(url) returns a promise for the server's response, which Lesson 03 uses.
  • Express and Fastify: a route handler can be an async function, and the framework waits for its promise before it sends the reply.
  • Playwright: every browser action in a test, such as await page.click("text=Login"), is a promise you await.

Common mistakes

1. Printing the promise instead of its value

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const price = sleep(10).then(() => 120);
console.log(price);
console.log(price + 30);
Promise { <pending> }
[object Promise]30

The first print shows a pending promise, not 120. The second is worse: adding a number to a promise turns it into the text [object Promise]. The value lives inside the token. Read it with price.then(...), or with await price inside an async function.

2. await outside an async function

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function wait() {
  await sleep(10);
  console.log("done");
}
wait();

Node refuses the file with SyntaxError: await is only valid in async functions and the top level bodies of modules. await only works inside a function marked async. In a CommonJS file like the Playground's main.js, the top level is not such a place either. Write async function wait(), and the file runs.

3. An executor that never settles

const ready = new Promise((resolve) => {
  console.log("cooking");
});

ready.then(() => console.log("served"));
console.log("waiting at the counter");
cooking
waiting at the counter

"served" never prints. The executor never calls resolve, so the promise stays pending for ever, and its then never runs. Node does not warn you. It simply exits when nothing is left to do. Every path through an executor must press resolve or reject.

Brain teaser

Which line comes first?

Zara mixes plain lines with a promise that is already fulfilled. She claims the then must run first, because nothing has to wait.

const ready = Promise.resolve("tea");
ready.then((drink) => console.log("2: " + drink));
console.log("1: order taken");

Is she right? Think about when a then callback is allowed to run.

Exercise 1Easy

Rafi wants a slow reader for his class. The program prints the words of a sentence one by one, with a pause between them, and then prints the line done.

Input. n, then a pause in milliseconds, then n words.

Output. each word on its own line, in the order given, then done.

Constraints. 1 <= n <= 10. The pause is 1 to 20. Each word is 1 to 10 letters.

Sample. the input below.

3 15
amar sonar bangla
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample output.

amar
sonar
bangla
done
Run in Compiler
Exercise 2Medium

Nadia's kitchen cooks several dishes at once. Each dish has a cooking time. Start every dish at the same moment, each as its own promise, and print each name when its promise fills.

Input. n, then n lines, each a dish name and its cooking time in milliseconds.

Output. the dish names in the order they finish.

Constraints. 1 <= n <= 8. The times are distinct multiples of 10, from 10 to 150.

Sample. the input below.

3
dal 40
bhaji 10
roti 20
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample output.

bhaji
roti
dal
Run in Compiler
Exercise 3Hard

Maria wants to see a chain decide its own fate. She starts with a number and builds a promise chain from commands. add k adds k in a then. fail throws an error in a then. fix k is a catch that recovers with the value k. Print how the chain ends.

Input. a starting number, then n, then n commands, each on its own line.

Output. one line: fulfilled v with the final value, or rejected if the chain ends rejected.

Constraints. 1 <= n <= 20. Every number is from 0 to 1000.

Sample. the input below.

5
4
add 3
fail
add 100
fix 1
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample output.

fulfilled 1
Run in Compiler

Common doubts

  • Is a promise the same as a callback?

    No, though it uses callbacks. With a plain callback, you hand your function to the slow code, and you trust it to call you once. With a promise, the slow code hands you a token, and you attach your function to it. The promise makes sure your function runs at most once.

  • Does await block the whole program?

    No. It pauses only the async function it sits in. The caller and the rest of the file keep running, and the loop keeps taking turns. That is why "countdown started" printed early above.

  • Can I get the value out of a promise without then or await?

    No. There is no p.value to read. The value reaches your code only through then, catch, finally or await. This is on purpose: it stops you from reading a value before it exists.

  • Why does Node print a promise as Promise { ... }?

    That is Node's own picture of the object for debugging. It shows the state and, once settled, the value. Your code cannot use that picture. It is only there to help you look.

Key takeaways

  • A slow function cannot return its answer, because the answer arrives after it returns; it returns a promise instead.
  • A promise is pending, then fulfilled with a value or rejected with a reason, and it settles once.
  • then runs a function when the value arrives and returns a new promise, so steps chain flat.
  • catch handles a rejection that fell down the chain, and the chain carries on after it.
  • await inside an async function pauses only that function until the promise settles.
  • Go deeper: Under the Hood, Microtasks, the Turns an await Costs, Thenables and Unhandled Rejections (Pro).

Next: every promise method one by one, and the rules for what each then returns.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Every Promise Method, One by One, With What It Returns