IntermediateTypeScript · Lesson 4 of 9

Asynchronous Code: Promises & async/await

Handle slow operations (network, files, timers) without blocking Node.js.

Node.js runs your JavaScript on a single thread. Slow operations — HTTP requests, database queries, file reads — are asynchronous: they return a Promise and Node.js keeps doing other work until the result is ready.

async functions always return a Promise; inside them, await pauses that function (not the whole program) until a Promise settles.

Run independent operations in parallel with Promise.all, and use Promise.allSettled when some may fail and you still want the others. Awaiting in a loop runs things one after another, which is slower when the tasks don't depend on each other.

async.tsTypeScript
import { setTimeout as sleep } from "node:timers/promises";

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

async function fetchTodo(id: number): Promise<Todo> {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos/" + id);
  if (!res.ok) throw new Error("HTTP " + res.status);
  return (await res.json()) as Todo;
}

async function slowSquare(n: number): Promise<number> {
  await sleep(300);
  return n * n;
}

async function main(): Promise<void> {
  console.time("parallel");
  const squares = await Promise.all([1, 2, 3].map(slowSquare)); // ~300ms, not 900ms
  console.timeEnd("parallel");
  console.log(squares);

  const results = await Promise.allSettled([fetchTodo(1), fetchTodo(2)]);
  for (const r of results) {
    if (r.status === "fulfilled") console.log("OK:", r.value.title);
    else console.log("Failed:", r.reason);
  }
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});

Key points

  • await only pauses the current async function — the process keeps serving other work.
  • Use Promise.all for independent tasks; avoid sequential await in loops when order doesn't matter.
  • Always handle rejections: try/catch inside, or .catch on the top-level call.

Exercise

Fetch users 1–5 from https://jsonplaceholder.typicode.com/users/{id} in parallel and print their names and cities. Then add a timeout: if a request takes more than 2 seconds, fail it (hint: AbortSignal.timeout(2000)).

Show solution

Try the exercise yourself first — then compare your approach with this one.

Start all five requests at once with map and wait for them together with Promise.all. AbortSignal.timeout(2000) cancels any request that takes longer than two seconds; the catch reports it instead of crashing.

users.tsTypeScript
const BASE_URL = "https://jsonplaceholder.typicode.com";

interface User {
  id: number;
  name: string;
  address: { city: string };
}

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(BASE_URL + "/users/" + id, { signal: AbortSignal.timeout(2000) });
  if (!res.ok) throw new Error("HTTP " + res.status + " for user " + id);
  return (await res.json()) as User;
}

async function main(): Promise<void> {
  try {
    const users = await Promise.all([1, 2, 3, 4, 5].map(fetchUser));
    for (const u of users) console.log(u.name + " - " + u.address.city);
  } catch (err) {
    if (err instanceof Error && err.name === "TimeoutError") console.log("A request took longer than 2 seconds");
    else console.log("Failed:", err instanceof Error ? err.message : err);
  }
}

main();

Check your understanding

  1. What does await pause?

  2. Three independent requests each take 300 ms. Roughly how long does await Promise.all([...]) take?

  3. When should you use Promise.allSettled instead of Promise.all?

  4. What is wrong with for (const id of ids) { results.push(await fetchUser(id)); } for independent requests?

Ask AI