Skip to content
Topic 19 of 21
Theory

async/await and fetch

Why it matters

Promises solved "callback hell", but then chains are still harder to read than ordinary code. The keywords async and await let you write asynchronous code as if it were synchronous: top to bottom, with ordinary variables and an ordinary try … catch.

And fetch is what all this was for: requests to a server. Every site that shows the weather, a news feed, products or messages gets them through requests. In React data is loaded exactly like this: useEffect + fetch + await.

An async function

The word async before a function does two things:

  1. the function always returns a promise: return 5 turns into a promise with the value 5;
  2. you can write await inside it.
async function getNumber() {
  return 5;
}

const p = getNumber();
console.log(p instanceof Promise);
p.then((value) => console.log(value));

Output:

true
5

Arrow functions can be asynchronous too: const load = async (id) => { ... };

await — wait for a promise

await promise pauses this function until the promise is fulfilled and returns its value:

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

async function main() {
  console.log("Started");
  const name = await wait(100, "Ann");          // wait 100 ms
  const city = await wait(100, "Dushanbe");     // another 100 ms
  console.log(name + " from " + city);
}

main();
console.log("main started but hasn't finished yet");

Output:

Started
main started but hasn't finished yet
Ann from Dushanbe

Important: await doesn't stop the whole program. It pauses only its own async function, and the rest of the code goes on. That's why the second line was printed before the third.

This is the same code as a then chain, just written differently: everything after await is as if inside then.

Errors: try … catch

If the promise under await is rejected, await throws the error, like throw. You catch it with an ordinary try … catch:

const fail = () => new Promise((_, reject) => setTimeout(() => reject(new Error("server unavailable")), 50));

async function main() {
  try {
    await fail();
    console.log("we won't get here");
  } catch (err) {
    console.log("Error: " + err.message);
  } finally {
    console.log("Loading finished");
  }
}

main();

Output:

Error: server unavailable
Loading finished

One by one or in parallel

Two awaits in a row run one after the other: the second request starts only when the first one has finished. If the requests don't depend on each other, that's needless waiting. Start them at once and wait for them together with Promise.all:

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

async function sequential() {
  const a = await wait(100, "A");
  const b = await wait(100, "B");
  return [a, b];
}

async function parallel() {
  const [a, b] = await Promise.all([wait(100, "A"), wait(100, "B")]);
  return [a, b];
}

async function main() {
  let start = Date.now();
  await sequential();
  console.log("One by one takes over 190 ms: " + (Date.now() - start >= 190));
  start = Date.now();
  await parallel();
  console.log("In parallel takes under 190 ms: " + (Date.now() - start < 190));
}

main();

Output:

One by one takes over 190 ms: true
In parallel takes under 190 ms: true

The rule: if the next step needs the result of the previous one — await one by one. If not — Promise.all.

await in a loop

for (const id of ids) {
  const user = await loadUser(id);   // one by one: each waits for the previous
}

const users = await Promise.all(ids.map((id) => loadUser(id)));   // all at once

But ids.forEach(async (id) => { await ... }) is a trap: forEach doesn't wait for async callbacks, and the code after it runs before the loads have finished.

fetch — a request to a server

fetch(url) sends an HTTP request and returns a promise of the response (Response). The response body is read separately and also asynchronously — that's why await appears twice:

async function loadTodo() {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  console.log(response.status);       // 200 — the response code
  console.log(response.ok);           // true if the code is 200–299
  const todo = await response.json(); // response body: JSON → object
  console.log(todo.title);
}

loadTodo();

The result looks roughly like this (it's a real practice server):

200
true
delectus aut autem

What a response has:

Property / method What it gives
response.status response code: 200, 201, 404, 500…
response.ok true if the code is from 200 to 299
await response.json() the body, parsed from JSON
await response.text() the body as a string
response.headers.get("Content-Type") a response header

The most common codes: 200 — all good, 201 — created, 400 — bad request, 401 / 403 — no access, 404 — not found, 500 — server error.

A network error and a server error are different things

fetch rejects its promise only when there's no response at all: no internet, the server is unavailable, the address is wrong. A 404 or 500 response is a successfully received response with a bad code. You have to check it yourself:

async function loadJSON(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

Without the response.ok check, the program will try to parse an error page as data.

Sending data: POST

To send data (create a todo, log in, leave a comment), you pass the request settings as the second argument:

async function addComment(text) {
  const response = await fetch("https://api.example.com/comments", {
    method: "POST",                                    // GET by default
    headers: { "Content-Type": "application/json" },   // tell the server: the body is JSON
    body: JSON.stringify({ text }),                    // the body is a string
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();   // the server usually sends back the created object
}

The main HTTP methods: GET — get, POST — create, PUT / PATCH — change, DELETE — delete. Together they form a REST API — that's how most servers you'll work with are built.

Typical data loading

Here is a pattern you'll meet in every application — React included. Loading has three states: in progress, succeeded, error:

async function showUsers() {
  console.log("Loading...");
  try {
    const users = await loadJSON("https://jsonplaceholder.typicode.com/users");
    console.log("Users: " + users.length);
  } catch (err) {
    console.log("Failed to load: " + err.message);
  } finally {
    console.log("Hiding the loading indicator");
  }
}

In React these three states will become three state variables: loading, data and error.

How it works inside

async/await is "syntactic sugar" over promises. On meeting await, the engine remembers where the function stopped and leaves it, returning a promise. The rest of the code keeps running. When the awaited promise is fulfilled, the continuation of the function is put into the microtask queue and runs from the same place, with all the same variables.

fetch performs the request itself using the browser or Node.js — outside JavaScript. The browser has a CORS restriction: a page can read responses from someone else's server only if that server allowed it with special headers. That's why a request that works in Node.js sometimes gives an error in the browser.

Common mistakes

  1. A forgotten await. const user = fetch(url); — user holds a promise, not the response. And response.json() without await is a promise, not data.
  2. await outside an async function. In an ordinary function await is a syntax error. (In modules and in this course's tasks await is allowed at the top level too.)
  3. No response.ok check. A 404 response won't land in catch by itself.
  4. Needless sequencing. Independent requests with await one after another are slow. Use Promise.all.
  5. forEach with async. It doesn't wait. Use for … of or Promise.all with map.
  6. Forgot JSON.stringify for body. You can't pass an object directly — the body is only a string.

Cheat sheet

What How
Async function async function f() { ... }, async () => { ... }
Wait const x = await promise;
Error try { await ... } catch (err) { ... }
In parallel const [a, b] = await Promise.all([p1, p2]);
GET const res = await fetch(url); const data = await res.json();
Check if (!res.ok) throw new Error(HTTP ${res.status});
POST fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj) })

User name

Easy+10 XP

Write an async function getUserName(id). It makes a fetch request to https://api.example.com/users/<id>, parses the response as JSON (it's an object { id, name }) and returns name.

await getUserName(1)   → "Ann"
await getUserName(2)   → "Timur"

Connect the connector to solve tasks

Task code runs on your own computer — faster and more reliable than in the browser. This needs the free CodeWithMe connector app. You only connect it once.

Looking for the connector on this computer…

  1. 1
    Download the connector for your system (Windows, macOS or Linux).Download the connectorOn the download page pick the file with your system in its name, e.g. windows-amd64.
  2. 2Run it and choose a folder for your solutions. It will install Python and Node.js there by itself when needed.
  3. 3
    Enter below the 6-digit code shown in the connector window (“Code for CodeWithMe”). If the browser asks about local network access — allow it.

The connector is open source (on GitHub), works only on your computer and runs code only after you enter the pairing code.