Promises and async/await

Asynchronous work, error handling, and the model behind data fetching.

Asking a server for a show takes time. fetch starts that work now and gives you a promise for the reply later.

then and catch

A promise starts as pending. Later it is fulfilled with a value, or rejected with an error. .then runs your function once the value arrives.

const promise = fetch(
  "https://api.tvmaze.com/search/shows?q=severance",
);

console.log("the promise itself:", promise);

promise
  .then((response) => response.json())
  .then((results) => console.log("the title:", results[0].show.name));

console.log("this line runs first");

The promise itself is not the show. It prints as pending, because the reply arrives later. The last line runs before it.

response.json() is a second promise. That is why there are two .then calls: first the HTTP reply, then the show data.

A chain and a missing show

Returning a value from a .then passes it into the next .then. A thrown error skips every later .then and goes to .catch.

fetch only rejects when the network fails. A 404 is still a reply, so you check response.ok yourself.

function loadShow(id) {
  return fetch("https://api.tvmaze.com/shows/" + id).then((response) => {
    if (!response.ok) {
      throw new Error("Request failed with " + response.status);
    }
    return response.json();
  });
}

loadShow(44933)
  .then((show) => {
    console.log("first:", show.name);
    return show.name.toUpperCase();
  })
  .then((loud) => {
    console.log("second:", loud);
    return loadShow(99999999);
  })
  .then((show) => console.log("never printed:", show.name))
  .catch((error) => console.log("caught:", error.message));

Show 44933 is Severance. The second .then received the value the first one returned. The missing id is a 404, so never printed: never appeared.

async and await

await waits for a promise and gives you its value. It only works inside a function marked async.

async function load() {
  const response = await fetch(
    "https://api.tvmaze.com/search/shows?q=severance",
  );
  const results = await response.json();
  console.log("with await:", results[0].show.name);

  const forgotten = fetch(
    "https://api.tvmaze.com/search/shows?q=andor",
  );
  console.log("without await:", forgotten);

  return results[0].show.name;
}

load().then((value) => console.log("load() gave:", value));

The first log is the title. The second forgot await, so it is still a pending promise. An async function always returns a promise, so load() is read with .then.

Errors with try/catch

await turns a rejection into a thrown error, so ordinary try/catch handles it.

async function load() {
  try {
    const response = await fetch("https://api.tvmaze.com/shows/99999999");
    if (!response.ok) {
      throw new Error("Request failed with " + response.status);
    }
    const show = await response.json();
    console.log("never printed:", show.name);
  } catch (error) {
    console.log("caught:", error.message);
  } finally {
    console.log("finally always runs");
  }
}

load();

The line after the failed check never ran. The finally block runs either way.

Sequential vs parallel

Two awaits in a row wait one after the other. If neither result needs the other, that is wasted time.

async function searchName(query) {
  const response = await fetch(
    "https://api.tvmaze.com/search/shows?q=" + query,
  );
  const results = await response.json();
  return results[0].show.name;
}

async function oneAtATime() {
  const start = Date.now();
  const first = await searchName("severance");
  const second = await searchName("andor");
  console.log(
    "one at a time:",
    first,
    second,
    "in",
    Date.now() - start,
    "ms",
  );
}

async function bothAtOnce() {
  const start = Date.now();
  const titles = await Promise.all([
    searchName("severance"),
    searchName("andor"),
  ]);
  console.log(
    "both at once:",
    titles.join(" "),
    "in",
    Date.now() - start,
    "ms",
  );
}

oneAtATime().then(bothAtOnce);

The first line waits for Severance, then Andor. The second line asks for both at once. Promise.all keeps the results in the order you listed the promises.

Promise.all rejects as soon as any one promise rejects. When you want every result either way, use Promise.allSettled.

Common mistakes

  • Forgetting await. You get a pending promise instead of the value.
  • Assuming fetch fails on 404. It does not. Check response.ok.
  • Awaiting one search at a time. If the searches do not depend on each other, use Promise.all.

Exercise

loadAll searches for three shows one after another. Make it search for all three at once, keeping the same order.

The first line must still read:

titles: Severance, Andor, The Bear

The second line should take about as long as one search, not three. Hint: Promise.all takes an array of promises.

async function searchName(query) {
  const response = await fetch(
    "https://api.tvmaze.com/search/shows?q=" + query,
  );
  const results = await response.json();
  return results[0].show.name;
}

async function loadAll() {
  const start = Date.now();

  const first = await searchName("severance");
  const second = await searchName("andor");
  const third = await searchName("the bear");

  console.log("titles:", [first, second, third].join(", "));
  console.log("took about", Date.now() - start, "ms");
}

loadAll();