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.
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
awaitonly pauses the current async function — the process keeps serving other work.- Use
Promise.allfor independent tasks; avoid sequentialawaitin loops when order doesn't matter. - Always handle rejections:
try/catchinside, or.catchon 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.
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();