Async cheatsheet.

Promise basics

const p = new Promise<number>((resolve, reject) => {
  setTimeout(() => resolve(42), 100);
});

p.then((v) => v + 1).catch((e) => console.error(e));

const v = await p;     // 42

async / await

async function f(): Promise<number> {
  const x = await fetch1();
  const y = await fetch2(x);
  return y;
}

async functions always return Promise. await only valid inside async (or top-level in ESM modules).

Promise.all

const [users, posts, tags] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
  fetchTags(),
]);

If ANY rejects, the whole thing rejects.

Promise.allSettled

const results = await Promise.allSettled([
  risky1(),
  risky2(),
]);

for (const r of results) {
  if (r.status === "fulfilled") console.log(r.value);
  else console.error(r.reason);
}

Never rejects.

Promise.race / Promise.any

// First to settle (resolve OR reject):
const winner = await Promise.race([p1, p2, p3]);

// First to fulfill (skips rejections):
const success = await Promise.any([p1, p2, p3]);

Sequential vs concurrent

// Sequential (slow)
for (const url of urls) {
  results.push(await fetch(url));
}

// Concurrent
const results = await Promise.all(urls.map(fetch));

Bounded concurrency (manual)

async function pool<T, R>(
  items: T[],
  limit: number,
  fn: (item: T) => Promise<R>,
): Promise<R[]> {
  const results: R[] = [];
  const executing: Promise<void>[] = [];
  
  for (const [i, item] of items.entries()) {
    const p = fn(item).then((r) => { results[i] = r; });
    executing.push(p);
    if (executing.length >= limit) {
      await Promise.race(executing);
      executing.splice(executing.findIndex(p => p), 1);
    }
  }
  
  await Promise.all(executing);
  return results;
}

await pool(urls, 5, fetch);

Or use p-limit:

import pLimit from "p-limit";
const limit = pLimit(5);
const results = await Promise.all(
  urls.map((u) => limit(() => fetch(u))),
);

Timeout

function timeout<T>(p: Promise<T>, ms: number): Promise<T> {
  return Promise.race([
    p,
    new Promise<never>((_, rej) => setTimeout(() => rej(new Error("timeout")), ms)),
  ]);
}

await timeout(fetch("/x"), 5000);

AbortSignal.timeout (Node 18+):

await fetch("/x", { signal: AbortSignal.timeout(5000) });

AbortController

const ctrl = new AbortController();

const p = fetch("/x", { signal: ctrl.signal });
setTimeout(() => ctrl.abort(), 1000);

try {
  await p;
} catch (e) {
  if (e instanceof DOMException && e.name === "AbortError") {
    // handled
  }
}

AbortSignal.any (combine):

const sig = AbortSignal.any([ctrl.signal, AbortSignal.timeout(5000)]);
await fetch("/x", { signal: sig });

Retry with backoff

async function retry<T>(
  fn: () => Promise<T>,
  attempts = 3,
  base = 100,
): Promise<T> {
  let lastErr: unknown;
  for (let i = 0; i < attempts; i++) {
    try { return await fn(); }
    catch (e) {
      lastErr = e;
      await new Promise(r => setTimeout(r, base * 2 ** i));
    }
  }
  throw lastErr;
}

await retry(() => fetch("/x").then(r => r.json()));

Deferred

function deferred<T>() {
  let resolve!: (v: T) => void;
  let reject!: (e: unknown) => void;
  const promise = new Promise<T>((res, rej) => {
    resolve = res; reject = rej;
  });
  return { promise, resolve, reject };
}

const d = deferred<number>();
setTimeout(() => d.resolve(42), 100);
const v = await d.promise;

Promise → callback bridge

import { promisify } from "node:util";
import fs from "node:fs";

const readFile = promisify(fs.readFile);
const buf = await readFile("a.txt");

Top-level await

// In an ESM module
const config = await loadConfig();
export { config };

Requires "type": "module" and target: ES2022+.

Async iterators

async function* range(n: number) {
  for (let i = 0; i < n; i++) {
    yield i;
    await new Promise(r => setTimeout(r, 100));
  }
}

for await (const x of range(5)) {
  console.log(x);
}

Async queue

class Queue<T> {
  private items: T[] = [];
  private waiters: Array<(v: T) => void> = [];
  
  push(item: T) {
    const w = this.waiters.shift();
    if (w) w(item);
    else this.items.push(item);
  }
  
  pop(): Promise<T> {
    const item = this.items.shift();
    if (item !== undefined) return Promise.resolve(item);
    return new Promise(res => this.waiters.push(res));
  }
}

Unhandled rejection

process.on("unhandledRejection", (reason) => {
  console.error("unhandled:", reason);
});

Always .catch() or await your promises.

Common mistakes

  • await in a forEach — doesn’t wait. Use for...of.
  • Promise.all([...]) with side effects — if one fails, others still run.
  • Forgetting AbortSignal — leaks long-running requests.
  • Returning a promise from a non-async function but not handling rejection.
  • try/catch only around await — rejection in fire-and-forget escapes.

Read this next

If you want my async utilities (pool, retry, queue), they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .