cheat sheet

Async / Await

async/await syntax, error handling, parallel execution with Promise.all, sequential vs parallel loops, top-level await, AbortController, and common mistakes.

Async / Await

What it is

async/await is syntactic sugar over Promises introduced in ES2017 (ES8). It lets you write asynchronous code that reads top-to-bottom like synchronous code, while still being non-blocking under the hood.

An async function always returns a Promise. Inside it, await suspends execution of that function — yielding control back to the event loop — until the awaited Promise settles, then resumes with the resolved value.

Basic syntax

Mark a function with async to make it return a Promise automatically; use await inside it to pause execution until a Promise settles. Both the function declaration form and arrow functions support async.

javascript
// async function declaration
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;                          // automatically wrapped in Promise.resolve()
}

// async arrow function
const fetchUser = async (id) => {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
};

// async function expression
const fetchUser = async function (id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
};

Calling an async function:

javascript
// Must await the result or chain .then()
const user = await fetchUser(42);
console.log(user.name);

// Or with .then() (same Promise underneath)
fetchUser(42).then(user => console.log(user.name));

Return value

An async function always returns a Promise, regardless of what you return inside it:

javascript
async function one() { return 1; }

one();              // Promise { 1 }
await one();        // 1

Returning another Promise from an async function does not double-wrap it — JavaScript unwraps (assimilates) it:

javascript
async function fetchData() {
  return fetch('/api/data');   // returns a Promise<Response>, not Promise<Promise<Response>>
}

Error handling

try / catch / finally

Use try/catch inside an async function to handle both synchronous throws and rejected await expressions in one block. finally always runs — useful for cleanup regardless of success or failure.

javascript
async function loadConfig(path) {
  try {
    const text = await fs.promises.readFile(path, 'utf8');
    return JSON.parse(text);
  } catch (error) {
    if (error.code === 'ENOENT') {
      console.warn('Config file not found, using defaults');
      return {};
    }
    throw error;          // re-throw errors you can't handle here
  } finally {
    console.log('loadConfig finished');   // always runs, even if error is re-thrown
  }
}

Output (file missing):

text
Config file not found, using defaults
loadConfig finished

Catching without try/catch — .catch() on the call site

javascript
const config = await loadConfig('./settings.json').catch(() => ({}));

Re-throw with context

javascript
async function getUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  } catch (err) {
    throw new Error(`getUser(${id}) failed: ${err.message}`, { cause: err });
  }
}

Parallel execution

Common anti-pattern: sequential awaits when order doesn't matter

javascript
// SLOW — runs one at a time, each waits for the previous to finish
async function loadDashboard(userId) {
  const user    = await fetchUser(userId);       // ~200ms
  const posts   = await fetchPosts(userId);      // ~150ms
  const friends = await fetchFriends(userId);    // ~120ms
  return { user, posts, friends };               // total: ~470ms
}

Promise.all — run in parallel, fail fast

Accepts an array of Promises and resolves with an array of their values once every one fulfills. If any single Promise rejects, the whole Promise.all rejects immediately and the other results are discarded.

javascript
// FAST — all three start simultaneously
async function loadDashboard(userId) {
  const [user, posts, friends] = await Promise.all([
    fetchUser(userId),
    fetchPosts(userId),
    fetchFriends(userId),
  ]);
  return { user, posts, friends };   // total: ~200ms (longest single request)
}

Promise.all rejects immediately if any of its promises rejects. If one fails, you lose the results of all the others. Use Promise.allSettled when you need partial results.

Promise.allSettled — run in parallel, collect all results

Like Promise.all but never short-circuits — it always waits for every Promise to settle, then returns an array of { status, value } / { status, reason } descriptors. Use it when partial success is acceptable.

javascript
async function loadDashboardSafe(userId) {
  const results = await Promise.allSettled([
    fetchUser(userId),
    fetchPosts(userId),
    fetchFriends(userId),
  ]);

  const [userResult, postsResult, friendsResult] = results;

  return {
    user:    userResult.status    === 'fulfilled' ? userResult.value    : null,
    posts:   postsResult.status   === 'fulfilled' ? postsResult.value   : [],
    friends: friendsResult.status === 'fulfilled' ? friendsResult.value : [],
  };
}

Output (postsResult rejects):

text
{ user: { id: 1, name: 'Jay' }, posts: [], friends: [{ id: 2, name: 'Alex' }] }

Promise.race — first one wins

Resolves or rejects with the outcome of whichever Promise settles first, regardless of whether it fulfilled or rejected. The canonical use case is pairing a real request with a timeout Promise.

javascript
// Timeout pattern
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

const user = await withTimeout(fetchUser(42), 3000);

Promise.any — first fulfillment wins

Resolves with the first Promise that fulfills; rejected Promises are ignored unless all of them reject, in which case it throws an AggregateError. Use it to race redundant endpoints and take the fastest successful response.

javascript
// Try multiple mirrors; use whichever responds first
const data = await Promise.any([
  fetch('https://mirror-1.example.com/data'),
  fetch('https://mirror-2.example.com/data'),
  fetch('https://mirror-3.example.com/data'),
]);

await in loops

Sequential: for...of with await

javascript
const userIds = [1, 2, 3, 4, 5];

// Processes one at a time — use when order matters or rate-limiting is needed
for (const id of userIds) {
  const user = await fetchUser(id);
  console.log(user.name);
}

Output:

text
Alice
Bob
Carol
Dave
Eve

Parallel: Promise.all + Array.map

javascript
// Fires all requests simultaneously
const users = await Promise.all(userIds.map(id => fetchUser(id)));
users.forEach(u => console.log(u.name));

Parallel with concurrency limit

javascript
// Process in batches of N to avoid overwhelming the server
async function batchProcess(items, batchSize, fn) {
  const results = [];
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const batchResults = await Promise.all(batch.map(fn));
    results.push(...batchResults);
  }
  return results;
}

const users = await batchProcess(userIds, 3, fetchUser);

Anti-pattern: await inside Array.forEach

javascript
// WRONG — forEach does not await; all iterations start but are not awaited
userIds.forEach(async (id) => {
  const user = await fetchUser(id);   // this await is inside an async callback
  console.log(user.name);             // may print out of order or after script ends
});
// execution continues here immediately, before any fetch completes

Use for...of or Promise.all + map instead.

Top-level await (ESM only)

In ES modules (.mjs or "type": "module") you can await at the top level of a file — outside any function:

javascript
// config.mjs
const response = await fetch('https://api.example.com/config');
export const config = await response.json();
javascript
// db.mjs
import pg from 'pg';
export const pool = await new pg.Pool({ connectionString: process.env.DATABASE_URL }).connect();

This is not available in CommonJS. If you use top-level await in a .js file, Node.js must determine it is ESM (via "type": "module" in package.json or the .mjs extension).

Async IIFE pattern

When you need await at the top level of a CJS script (no top-level await available), wrap the entry point in an immediately-invoked async function:

javascript
// CJS script — top-level await unavailable
(async () => {
  const config = await loadConfig();
  const db = await connectDatabase(config);
  await startServer(db);
})().catch(err => {
  console.error('Fatal error:', err);
  process.exit(1);
});

Async class methods

Any method on a class can be async. There is no async getter or setter (because getters are synchronous by definition).

javascript
class UserService {
  async getUser(id) {
    return fetch(`/api/users/${id}`).then(r => r.json());
  }

  async createUser(data) {
    const res = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
    if (!res.ok) throw new Error(`Create failed: ${res.status}`);
    return res.json();
  }
}

const svc = new UserService();
const user = await svc.getUser(1);

Async getter workaround

Async getters are not a language feature. Use an async method named getXxx() or initialize via a factory:

javascript
class Config {
  // NOT: async get value() { ... }   — SyntaxError

  // Instead, use a static async factory
  static async create() {
    const instance = new Config();
    instance._data = await loadData();
    return instance;
  }

  get value() { return this._data; }   // synchronous after init
}

const config = await Config.create();
console.log(config.value);

AbortController — cancelling async operations

AbortController provides a standard way to cancel async operations (fetch calls, streaming, timers) before they complete.

javascript
const controller = new AbortController();
const { signal } = controller;

// Cancel after 5 seconds
const timeoutId = setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch('/api/large-file', { signal });
  const data = await response.json();
  clearTimeout(timeoutId);
  return data;
} catch (err) {
  if (err.name === 'AbortError') {
    console.log('Request was cancelled');
    return null;
  }
  throw err;
}

Attach the same signal to multiple operations to cancel them together:

javascript
async function fetchBoth(signal) {
  const [a, b] = await Promise.all([
    fetch('/api/a', { signal }),
    fetch('/api/b', { signal }),
  ]);
  return { a: await a.json(), b: await b.json() };
}

const controller = new AbortController();
setTimeout(() => controller.abort(), 2000);
const result = await fetchBoth(controller.signal);

Common mistakes

Forgetting await

javascript
// WRONG — returns a Promise object, not the user
async function broken() {
  const user = fetchUser(1);    // missing await
  return user.name;             // TypeError: Cannot read properties of a Promise
}

// CORRECT
async function fixed() {
  const user = await fetchUser(1);
  return user.name;
}

Unhandled rejections

javascript
// WRONG — the rejection is unhandled; Node will log a warning and may exit
async function fire() {
  const result = await mightFail();
}
fire();   // Promise is floating — nobody catches its rejection
javascript
// CORRECT — always handle the rejection at the call site
fire().catch(err => console.error(err));

// Or await it in another async function with try/catch
try {
  await fire();
} catch (err) {
  console.error(err);
}

Sequential awaits when parallel would be faster

Already covered above — the most impactful performance mistake in async code. Default to Promise.all when results are independent.

await on a non-Promise value

This is harmless but unnecessary:

javascript
const x = await 42;          // x is 42 — works but pointless
const y = await null;        // y is null
const z = await someSync();  // z is the return value — no harm, no benefit

Async functions in setTimeout lose uncaught errors

javascript
// WRONG — async errors inside setTimeout are silently swallowed
setTimeout(async () => {
  await mightFail();   // rejection is lost
}, 1000);

// CORRECT — catch explicitly
setTimeout(() => {
  mightFail().catch(err => console.error('background task failed:', err));
}, 1000);

Mental model — what await actually does

An async function is a state machine generated by the engine. Each await is a point at which the function pauses, hands control back to the event loop, and registers a microtask to resume execution when the awaited Promise settles. While paused, other code runs — the engine is not blocked.

javascript
async function example() {
  console.log("A");
  const v = await Promise.resolve(1);   // pauses here, resumes as microtask
  console.log("B", v);
  return v + 1;
}

console.log("1");
const p = example();      // logs "A" synchronously, returns a Promise
console.log("2");
console.log("3", await p); // logs "B 1" then "3 2"

Output:

text
1
A
2
B 1
3 2

Two key consequences:

  • Code before the first await runs synchronously during the initial call. Putting validation or fast-fail checks above the first await lets them throw before any work starts.
  • Code after await runs as a microtask, after the current synchronous frame unwinds. State that was true at the start of the function may have changed by the time the next line executes.

Awaiting a non-Promise

await x first coerces x to a Promise via Promise.resolve(x). If x is already a Promise it is reused; otherwise await yields one microtask and resumes with x. This is why const y = await 42 "works" — it costs one microtask boundary for nothing.

javascript
async function pointlessAwait() {
  return await 42;       // ≡ return 42, but adds one microtask
}

The linter rule no-return-await exists for this reason: return await x in tail position is functionally equivalent to return x for the caller but adds an extra microtask. Exception: inside try/catch, return await is correct because dropping the await would let the rejection escape the try block.

javascript
// Correct use of return await — keeps catch in scope
async function safe() {
  try {
    return await mightReject();
  } catch (err) {
    return fallback();
  }
}

// WITHOUT await, rejection would skip the catch
async function broken() {
  try {
    return mightReject();   // catch is no longer in the call stack when reject fires
  } catch (err) {
    return fallback();      // never runs
  }
}

Async iteration — for await...of

for await...of consumes an async iterable — any object implementing [Symbol.asyncIterator](). Each iteration awaits the iterator's next value, pausing the loop until it settles. It is the idiomatic way to consume Node streams, paginated APIs, and any cursor-like source.

javascript
async function* asyncRange(n) {
  for (let i = 0; i < n; i++) {
    await new Promise((r) => setTimeout(r, 10));  // simulate I/O
    yield i;
  }
}

for await (const v of asyncRange(3)) {
  console.log(v);
}

Output:

text
0
1
2

Consuming a Node Readable stream

Every Readable stream is an async iterable since Node 10. Reading a file chunk-by-chunk is one for await away.

javascript
import { createReadStream } from 'node:fs';

for await (const chunk of createReadStream('input.log', { encoding: 'utf8' })) {
  process.stdout.write(chunk);
}

Output:

text
(contents of input.log)

The pattern carries the backpressure semantics of the stream — the loop pauses while the runtime is full, so memory stays bounded regardless of file size. See node-streams for the full picture.

Paginated APIs as async iterables

The classic shape: an async generator that yields each page and stops when the cursor runs out. Consumers see a flat iterable.

javascript
async function* paginate(url) {
  let cursor = null;
  do {
    const res = await fetch(`${url}?cursor=${cursor ?? ''}`);
    const page = await res.json();
    for (const item of page.items) yield item;
    cursor = page.nextCursor;
  } while (cursor);
}

let count = 0;
for await (const user of paginate('/api/users')) {
  count++;
  if (count >= 100) break;   // early stop closes the iterator
}
console.log(`processed ${count} users`);

Output:

text
processed 100 users

When the loop breaks, the async iterator's return() is called automatically, which runs any pending finally block in the generator — letting you close cursors and free resources cleanly.

Async iteration with concurrency

for await is strictly sequential. To process pages in parallel, decouple paging from work: produce a stream of items with the generator, then fan out a fixed-size pool of workers.

javascript
async function processWithConcurrency(asyncIter, limit, worker) {
  const inflight = new Set();
  for await (const item of asyncIter) {
    const p = worker(item).finally(() => inflight.delete(p));
    inflight.add(p);
    if (inflight.size >= limit) {
      await Promise.race(inflight);
    }
  }
  await Promise.all(inflight);
}

await processWithConcurrency(paginate('/api/users'), 5, async (user) => {
  console.log(user.id);
});

Async generators — async function*

An async generator function (async function*) returns an async iterator that supports both yield (produce a value) and await (consume a Promise). It is the natural shape for any data source that is both async and finite-or-streaming.

javascript
async function* lines(stream) {
  let buffer = '';
  for await (const chunk of stream) {
    buffer += chunk;
    let nl;
    while ((nl = buffer.indexOf('\n')) >= 0) {
      yield buffer.slice(0, nl);
      buffer = buffer.slice(nl + 1);
    }
  }
  if (buffer) yield buffer;
}

Generator-side semantics

  • yield x pauses the generator until the consumer calls next() again.
  • yield somePromise yields the Promise itself, not its resolved value. To yield the resolved value, write yield await somePromise.
  • A return from inside the generator becomes the iterator's final { value, done: true }. Subsequent next() calls return { value: undefined, done: true } forever.
  • Throws inside the generator propagate to the consumer's for await as a rejection.
  • The consumer's break/return triggers the generator's finally blocks before disposal.
javascript
async function* withCleanup() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log('generator cleanup');  // runs on break or normal exit
  }
}

for await (const v of withCleanup()) {
  if (v === 2) break;
  console.log(v);
}

Output:

text
1
generator cleanup

Parallel composition patterns

The default of await is sequential. Five recipes cover almost every parallelism need.

1. Independent work — Promise.all

When operations don't depend on each other and you need every result.

javascript
const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);

2. Tolerant of failures — Promise.allSettled

When you want every result regardless of which ones failed.

javascript
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
const ok = results.filter((r) => r.status === 'fulfilled').map((r) => r.value);

3. Fastest one wins — Promise.any

When you have redundant sources and need the first successful response.

javascript
const data = await Promise.any([fetchPrimary(), fetchSecondary(), fetchTertiary()]);

4. Race with a timeout — Promise.race

When you want a hard deadline regardless of which side wins.

javascript
function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms)
    ),
  ]);
}

5. Bounded pool — N at a time

When you want concurrency but capped at N. Reuse the pool from promises.

javascript
async function pool(items, limit, worker) {
  const results = new Array(items.length);
  let i = 0;
  async function next() {
    while (i < items.length) {
      const idx = i++;
      results[idx] = await worker(items[idx], idx);
    }
  }
  await Promise.all(Array.from({ length: limit }, next));
  return results;
}

Top-level await — runtime considerations

Top-level await works in ES modules (.mjs, or .js in a "type": "module" package). It blocks the importer, not the event loop — but if every module on the critical path has a await at the top, your application's startup is now serialised through them.

javascript
// db.mjs
export const db = await connectDatabase();

// users.mjs
import { db } from './db.mjs';        // pauses until db.mjs settles
export async function getUser(id) { return db.users.findOne({ id }); }

// app.mjs
import { getUser } from './users.mjs'; // pauses until users.mjs (and transitively db.mjs) settles

The dependency graph is evaluated in topological order; cycles with TLA can deadlock. See modules.

Top-level await is unavailable in:

  • CJS files (.cjs, or .js in a "type": "commonjs" package).
  • The default global scope of script-tag inclusion (<script> without type="module").
  • Inside a static block of a class.

In any of these contexts, wrap the entry point in an async IIFE as documented above.

await using — disposable resources (ES2024 / TC39 stage 4)

The using and await using declarations bind a resource to a block; when the block exits — by return, throw, or fall-through — the resource's Symbol.dispose or Symbol.asyncDispose runs automatically. This is the JS equivalent of Python's with and C#'s using.

javascript
class FileHandle {
  constructor(path) {
    this.path = path;
    console.log(`opening ${path}`);
  }
  async [Symbol.asyncDispose]() {
    console.log(`closing ${this.path}`);
  }
}

async function run() {
  await using f = new FileHandle('./data.txt');
  console.log('working with file');
  // f is async-disposed when this function returns
}

await run();

Output:

text
opening ./data.txt
working with file
closing ./data.txt

Multiple await using declarations dispose in reverse order — LIFO, matching how finally blocks unwind. Available in Node 22+, modern Chrome and Firefox; older runtimes need a transpiler (TypeScript 5.2+, Babel @babel/plugin-proposal-explicit-resource-management).

Microtask boundaries inside async functions

Every await is a microtask suspension point. Code that touched shared state can be subtly broken by an await in the middle, because between await x and the next line, other tasks may have mutated that state.

javascript
let inFlight = false;

async function clickHandler() {
  if (inFlight) return;
  inFlight = true;
  await doWork();        // microtask boundary — another click can fire here
  inFlight = false;
}

If two clicks fire fast enough, the second sees inFlight === true and returns — that's the desired behaviour. The trap is more subtle: the line after await reads inFlight, but cannot reset it to false until the awaited Promise settles, so a third click during doWork() is also rejected. That's usually fine for a debounce, but make the intent explicit:

javascript
async function clickHandler() {
  if (inFlight) return;
  inFlight = true;
  try {
    await doWork();
  } finally {
    inFlight = false;     // always reset, even on error
  }
}

TC39 async-generator suspension model

For deeper understanding, four moments matter inside an async function*:

  1. First call — calling the generator returns an async iterator immediately. No code in the body has run yet.
  2. next() call — runs the body up to the next yield (or end). Returns Promise<{ value, done }>.
  3. yield resumption — the next next() resumes the body with the value the consumer passed in.
  4. Disposalreturn() or thrown errors run any pending finally blocks.

This means the line await something() placed before the first yield does not run until the consumer's first next() — useful if expensive setup should be deferred until iteration actually begins.

Common anti-patterns (extended)

Awaiting in a tight CPU loop

javascript
// Awaiting a settled microtask millions of times starves the event loop
async function broken() {
  let sum = 0;
  for (let i = 0; i < 1_000_000; i++) {
    sum += await Promise.resolve(i);   // unnecessary microtask per iteration
  }
  return sum;
}

The awaits don't yield to I/O because microtasks drain before macrotasks. Worse: each iteration allocates a Promise that becomes garbage. Drop the await entirely when there's no real async work.

Implicit await in callback context

javascript
// .map's callback is sync; the async function below resolves to an array of Promises
async function broken(items) {
  return items.map(async (item) => await transform(item));
}

// Caller probably expected the resolved values — they get pending Promises
const arr = await broken([1, 2, 3]);
console.log(arr[0]);   // Promise { <pending> }

Fix with Promise.all:

javascript
async function fixed(items) {
  return Promise.all(items.map((item) => transform(item)));
}

Forgetting to await inside try

javascript
async function leak() {
  try {
    fetchData();   // missing await — rejection escapes the try
  } catch (err) {
    console.error("never runs");
  }
}

Reading variables across an await without remembering they may have changed

javascript
async function update(user) {
  const current = userCache.get(user.id);
  await persist(user);                    // await — userCache may have been mutated
  current.lastSaved = Date.now();         // current may be stale
}

Re-read state after every await when the value might have been touched by another task.

Real-world recipes

Reading a JSON-Lines file line-by-line

A streaming line iterator + JSON parse, bounded memory regardless of file size.

javascript
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';

const lines = createInterface({
  input: createReadStream('events.jsonl'),
  crlfDelay: Infinity,
});

let count = 0;
for await (const line of lines) {
  if (!line.trim()) continue;
  const event = JSON.parse(line);
  if (event.type === 'error') count++;
}
console.log(`${count} errors`);

Output:

text
247 errors

Background task with cancellation and cleanup

A long-running task that runs on an interval, can be stopped, and cleans up its resources.

javascript
async function runWorker(signal) {
  while (!signal.aborted) {
    try {
      const job = await dequeue({ signal });
      await processJob(job);
    } catch (err) {
      if (err.name === 'AbortError') break;
      console.error('job failed', err);
    }
  }
  console.log('worker stopped');
}

const controller = new AbortController();
const workerDone = runWorker(controller.signal);

process.on('SIGTERM', () => controller.abort());
await workerDone;

Retry with jitter

Exponential backoff with random jitter to avoid thundering herds.

javascript
async function retry(fn, { attempts = 5, base = 250, factor = 2, max = 10_000 } = {}) {
  for (let i = 0; ; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i >= attempts - 1) throw err;
      const delay = Math.min(base * factor ** i, max);
      const jittered = delay / 2 + Math.random() * (delay / 2);
      await new Promise((r) => setTimeout(r, jittered));
    }
  }
}

const data = await retry(() => fetch('/api/flaky').then((r) => r.json()));

Map-with-progress

Async map that reports progress as each item finishes.

javascript
async function mapWithProgress(items, fn, onProgress) {
  let done = 0;
  return Promise.all(
    items.map(async (item, i) => {
      const result = await fn(item, i);
      onProgress(++done, items.length);
      return result;
    })
  );
}

await mapWithProgress(
  ['a', 'b', 'c', 'd'],
  async (x) => x.toUpperCase(),
  (done, total) => console.log(`${done}/${total}`)
);

Output:

text
1/4
2/4
3/4
4/4

Composing multiple cancellable steps

Three sequential steps that share a single AbortController. Cancelling the controller stops whichever step is currently in flight.

javascript
async function pipeline(signal) {
  const a = await fetch('/api/step1', { signal }).then((r) => r.json());
  const b = await fetch(`/api/step2/${a.id}`, { signal }).then((r) => r.json());
  return fetch('/api/step3', {
    method: 'POST',
    body: JSON.stringify(b),
    signal,
  });
}

const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
try {
  await pipeline(controller.signal);
} catch (err) {
  if (err.name === 'AbortError') console.log('pipeline cancelled');
  else throw err;
}

Async semaphore

When you need a generic "no more than N concurrent operations" gate that any code can await acquire() against.

javascript
function semaphore(limit) {
  let active = 0;
  const queue = [];
  return {
    async acquire() {
      if (active < limit) {
        active++;
        return;
      }
      await new Promise((resolve) => queue.push(resolve));
      active++;
    },
    release() {
      active--;
      const next = queue.shift();
      if (next) next();
    },
  };
}

const sem = semaphore(3);

async function task(id) {
  await sem.acquire();
  try {
    await doWork(id);
  } finally {
    sem.release();
  }
}

await Promise.all(Array.from({ length: 20 }, (_, i) => task(i)));

Streaming JSON response

Process a fetch response as it arrives without buffering the whole body.

javascript
const response = await fetch('/api/large.jsonl');
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();

let buffer = '';
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  let nl;
  while ((nl = buffer.indexOf('\n')) >= 0) {
    const line = buffer.slice(0, nl);
    buffer = buffer.slice(nl + 1);
    if (line) handle(JSON.parse(line));
  }
}

Common pitfalls

  1. Sequential awaits when parallel would work — single most common performance bug in async code. Default to Promise.all when steps are independent.
  2. Missing await inside try — the rejection escapes the try block. Always await (or return) before the try closes.
  3. return await outside of try is redundant — it adds a microtask. Inside try/catch, it is required; outside, drop it.
  4. forEach with async callbacksforEach discards return values. Use for...of, for await...of, or Promise.all(arr.map(...)).
  5. Reading state across an await — the value may have been mutated by another microtask. Re-fetch or capture into a local before the await.
  6. Async getters and setters — not a language feature. Use an async method (getX()) or a static factory.
  7. Top-level await in CJSSyntaxError. Switch to ESM or wrap in an async IIFE.
  8. Awaiting in a synchronous reducearr.reduce((acc, x) => acc + await fn(x)) is a parse error in async code (the inner callback isn't async). Use for...of or fold over Promises.
  9. Cancellation without signal propagation — wrapping fetch in your own timeout race leaves the request running in the background. Pass signal through every layer that accepts it.
  10. Async constructor — class constructors cannot be async. Use a static create() factory that returns a Promise.