Comprehensive patterns for asynchronous JavaScript — Promises, async/await, iterators, streams, workers, and cancellation.
// Only use the constructor to wrap callback-based APIs
function readFileAsync(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// Anti-pattern: Promise constructor around another Promise
// BAD — "deferred anti-pattern"
const p = new Promise((resolve) => {
fetch('/api/data').then(resolve); // don't do this
});
// GOOD — just return the Promise directly
const p = fetch('/api/data');
fetch('/api/users')
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json(); // return value becomes next then's input
})
.then(users => console.log(users))
.catch(err => {
// catches ALL errors from any then() above
console.error('Failed:', err);
return []; // recover — return value continues the chain
})
.finally(() => {
// always runs, receives no argument, does NOT change resolved value
hideLoadingSpinner();
});
// BAD — nested (pyramid of doom with Promises)
fetch('/api/user')
.then(res => res.json().then(user => {
fetch(`/api/posts/${user.id}`).then(res => res.json().then(posts => {
console.log(posts);
}));
}));
// GOOD — flat chain
fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(res => res.json())
.then(posts => console.log(posts));
// BEST — async/await
const res = await fetch('/api/user');
const user = await res.json();
const postsRes = await fetch(`/api/posts/${user.id}`);
const posts = await postsRes.json();
// Runs in parallel, fails fast on first rejection
const [user, posts, comments] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchComments(userId),
]);
// With error handling — if any rejects, the whole thing rejects
try {
const results = await Promise.all([a(), b(), c()]);
} catch (err) {
// err is from whichever promise rejected first
// The other promises are NOT cancelled (still running!)
}
// Use case: multiple independent API calls that all must succeed
async function loadDashboard(userId) {
const [profile, analytics, notifications] = await Promise.all([
api.getProfile(userId),
api.getAnalytics(userId),
api.getNotifications(userId),
]);
return { profile, analytics, notifications };
}
// Never rejects — always resolves with array of outcome objects
const results = await Promise.allSettled([
fetch('/api/primary'),
fetch('/api/secondary'),
fetch('/api/tertiary'),
]);
for (const result of results) {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.warn('Failed:', result.reason);
}
}
// Use case: batch operations where partial success is acceptable
async function syncAllRecords(records) {
const results = await Promise.allSettled(
records.map(r => syncRecord(r))
);
const failures = results.filter(r => r.status === 'rejected');
if (failures.length > 0) {
console.warn(`${failures.length} of ${records.length} records failed to sync`);
}
return results;
}
// Resolves or rejects with the FIRST settled promise (fulfilled OR rejected)
const result = await Promise.race([
fetch('/api/data'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
),
]);
// Use case: implement timeout (prefer AbortSignal.timeout() in modern code)
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
// Use case: racing multiple endpoints (first one wins)
const data = await Promise.race([
fetchFromPrimary(),
fetchFromFallback(),
]);
// Resolves with FIRST fulfilled promise; rejects (AggregateError) only if ALL reject
try {
const data = await Promise.any([
fetchFromCDN1(),
fetchFromCDN2(),
fetchFromOrigin(),
]);
console.log('Got data from fastest source:', data);
} catch (err) {
// err is AggregateError — all failed
console.error('All sources failed:', err.errors);
}
// Use case: redundant data sources with automatic fallback
async function fetchWithFallback(urls) {
return Promise.any(urls.map(url => fetch(url).then(r => r.json())));
}
// SEQUENTIAL — each waits for the previous (slow if independent)
const user = await getUser(id);
const posts = await getPosts(id); // waits for getUser to finish first
const friends = await getFriends(id); // waits for getPosts to finish first
// PARALLEL — all start together (fast)
const [user, posts, friends] = await Promise.all([
getUser(id),
getPosts(id),
getFriends(id),
]);
// MIXED — some sequential dependencies, some parallel
const user = await getUser(id); // must come first
const [posts, preferences] = await Promise.all([
getPosts(user.blogId), // depends on user
getPreferences(user.settingsId), // depends on user
]);
// 1. try/catch — most readable
async function loadUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`, { cause: res });
return await res.json();
} catch (err) {
if (err.name === 'AbortError') return null; // cancelled — not an error
throw err; // re-throw unexpected errors
}
}
// 2. Inline error handling for parallel calls
const [userResult, postsResult] = await Promise.allSettled([
getUser(id),
getPosts(id),
]);
const user = userResult.status === 'fulfilled' ? userResult.value : null;
// 3. Utility: "safe" wrapper that returns [error, result]
async function safe(promise) {
try {
return [null, await promise];
} catch (err) {
return [err, null];
}
}
const [err, user] = await safe(getUser(id));
if (err) { /* handle */ }
// Works at the top level of an ES module — no async wrapper needed
// file: config.mjs
const config = await fetch('/api/config').then(r => r.json());
export const DB_URL = config.dbUrl;
export const API_KEY = config.apiKey;
// Importers of this module will wait for the top-level awaits to settle
// import { DB_URL } from './config.mjs'; // already resolved by the time you use it
// ES2024 — cleaner deferred pattern
const { promise, resolve, reject } = Promise.withResolvers();
// Classic use case: expose resolve/reject to external code
class EventQueue {
#waiters = [];
async waitForNext() {
const { promise, resolve } = Promise.withResolvers();
this.#waiters.push(resolve);
return promise;
}
emit(value) {
const resolve = this.#waiters.shift();
resolve?.(value);
}
}
// Use case: WebSocket message waiting
class WebSocketClient {
#pending = new Map();
#counter = 0;
send(data) {
const id = ++this.#counter;
const { promise, resolve, reject } = Promise.withResolvers();
this.#pending.set(id, { resolve, reject });
this.#ws.send(JSON.stringify({ id, data }));
return promise;
}
#onMessage(event) {
const { id, result, error } = JSON.parse(event.data);
const { resolve, reject } = this.#pending.get(id) ?? {};
this.#pending.delete(id);
if (error) reject(new Error(error));
else resolve(result);
}
}
// Before ES2024 — manual deferred
function deferred() {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
// Works with any object implementing Symbol.asyncIterator
async function processLargeFile(filePath) {
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
for await (const chunk of stream) {
await processChunk(chunk);
}
}
// Paginated API — async iterator wraps pagination logic
async function* paginate(url) {
let nextUrl = url;
while (nextUrl) {
const res = await fetch(nextUrl);
const { data, next } = await res.json();
yield* data; // yield each item individually
nextUrl = next; // null/undefined stops the loop
}
}
for await (const user of paginate('/api/users?limit=100')) {
await processUser(user);
}
// Async generator function — yields Promises
async function* generateNumbers(start, end, delayMs = 100) {
for (let i = start; i <= end; i++) {
await new Promise(r => setTimeout(r, delayMs));
yield i;
}
}
// Async generator for event-based sources
async function* fromEventTarget(target, event, { signal } = {}) {
const { promise, resolve } = Promise.withResolvers();
const handler = (e) => resolve(e);
target.addEventListener(event, handler, { once: true, signal });
try {
yield await promise;
// Recurse to get next event
yield* fromEventTarget(target, event, { signal });
} finally {
target.removeEventListener(event, handler);
}
}
// Consuming with early exit
const controller = new AbortController();
for await (const event of fromEventTarget(emitter, 'data', { signal: controller.signal })) {
processEvent(event);
if (shouldStop) controller.abort();
}
class AsyncRange {
constructor(start, end, delay = 0) {
this.start = start;
this.end = end;
this.delay = delay;
}
[Symbol.asyncIterator]() {
let current = this.start;
const { end, delay } = this;
return {
async next() {
if (delay) await new Promise(r => setTimeout(r, delay));
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
return() {
// Called when consumer breaks early
return { value: undefined, done: true };
}
};
}
}
for await (const n of new AsyncRange(1, 5, 100)) {
console.log(n); // 1, 2, 3, 4, 5 with 100ms between each
}
// Cancel an in-flight fetch
const controller = new AbortController();
const button = document.querySelector('#cancel');
button.addEventListener('click', () => controller.abort());
try {
const res = await fetch('/api/large-export', {
signal: controller.signal,
});
const data = await res.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('Request was cancelled');
} else {
throw err;
}
}
// No need for Promise.race with a timeout Promise anymore
const res = await fetch('/api/data', {
signal: AbortSignal.timeout(5000), // throws TimeoutError after 5s
});
// TimeoutError is a subclass of DOMException
try {
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
} catch (err) {
if (err.name === 'TimeoutError') {
console.log('Request timed out');
}
}
// Cancel if EITHER the user aborts OR we time out
const userController = new AbortController();
const combined = AbortSignal.any([
userController.signal,
AbortSignal.timeout(10_000),
]);
const res = await fetch(url, { signal: combined });
// Make your own async functions respect abort signals
async function delay(ms, { signal } = {}) {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
}, { once: true });
});
}
// Propagate cancellation through a chain of operations
async function processItems(items, { signal } = {}) {
for (const item of items) {
signal?.throwIfAborted(); // throws if already aborted
await processItem(item, { signal });
}
}
// Create a readable stream from scratch
const stream = new ReadableStream({
start(controller) {
// Called once when stream is created
},
async pull(controller) {
// Called when consumer wants more data
const chunk = await getNextChunk();
if (chunk === null) {
controller.close();
} else {
controller.enqueue(chunk);
}
},
cancel(reason) {
// Called if consumer cancels
cleanup();
},
});
// Consume with for await
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
process(value);
}
} finally {
reader.releaseLock();
}
// Or use for-await-of (web streams are async iterable in modern browsers/Node 18+)
for await (const chunk of stream) {
process(chunk);
}
const writable = new WritableStream({
async write(chunk, controller) {
await persistChunk(chunk);
},
close() {
console.log('Stream finished');
},
abort(err) {
console.error('Stream aborted:', err);
},
});
const writer = writable.getWriter();
await writer.write('chunk 1');
await writer.write('chunk 2');
await writer.close();
// Parse newline-delimited JSON (NDJSON)
class NDJSONParser extends TransformStream {
constructor() {
let buffer = '';
super({
transform(chunk, controller) {
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop(); // last line may be incomplete
for (const line of lines) {
if (line.trim()) {
controller.enqueue(JSON.parse(line));
}
}
},
flush(controller) {
if (buffer.trim()) {
controller.enqueue(JSON.parse(buffer));
}
},
});
}
}
// Usage: fetch NDJSON and stream parsed objects
const response = await fetch('/api/export.ndjson');
const objectStream = response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new NDJSONParser());
for await (const record of objectStream) {
await insertRecord(record);
}
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
import { Transform } from 'node:stream';
// pipeline() properly handles errors and cleanup
await pipeline(
createReadStream('./input.csv'),
new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
// Transform each chunk
const processed = processCSVChunk(chunk.toString());
callback(null, processed);
},
}),
createGzip(),
createWriteStream('./output.csv.gz'),
);
// Writer respects backpressure automatically
const writer = writable.getWriter();
for (const chunk of largeDataset) {
// write() returns a Promise that resolves when ready for more
await writer.write(chunk);
}
// main.js
const worker = new Worker('./worker.js', { type: 'module' });
worker.postMessage({ type: 'COMPUTE', data: largeArray });
worker.addEventListener('message', (event) => {
console.log('Result:', event.data.result);
});
worker.addEventListener('error', (event) => {
console.error('Worker error:', event.message);
});
// Terminate when done
worker.terminate();
// worker.js
self.addEventListener('message', async (event) => {
const { type, data } = event.data;
if (type === 'COMPUTE') {
const result = heavyComputation(data);
self.postMessage({ result });
}
});
// Transfer ownership of ArrayBuffer — zero-copy, no serialization
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB
const view = new Uint8Array(buffer);
// After transfer, buffer is detached in the sender
worker.postMessage({ buffer }, [buffer]); // second arg = transferables
// buffer.byteLength === 0 now — ownership transferred
// In worker.js
self.addEventListener('message', ({ data: { buffer } }) => {
const view = new Uint8Array(buffer);
// ... process ...
// Transfer back
self.postMessage({ buffer }, [buffer]);
});
// SharedArrayBuffer — both threads read/write same memory
// Requires Cross-Origin Isolation: COOP + COEP headers
// main.js
const shared = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 10);
const array = new Int32Array(shared);
worker.postMessage({ shared }); // no transfer needed — it's shared
// Atomic operations prevent data races
Atomics.add(array, 0, 1); // atomic increment
const val = Atomics.load(array, 0); // atomic read
Atomics.store(array, 0, 42); // atomic write
const exchanged = Atomics.compareExchange(array, 0, expected, replacement);
// Atomic wait/notify (for coordination between threads)
// worker: wait for index 0 to change from 0
Atomics.wait(array, 0, 0); // blocks worker thread
// main: signal worker
Atomics.store(array, 0, 1);
Atomics.notify(array, 0, 1); // wake one waiter
const channel = new MessageChannel();
const worker1 = new Worker('./worker1.js');
const worker2 = new Worker('./worker2.js');
// Give each worker a port — they can now talk directly
worker1.postMessage({ port: channel.port1 }, [channel.port1]);
worker2.postMessage({ port: channel.port2 }, [channel.port2]);
// In worker1.js
self.addEventListener('message', ({ data: { port } }) => {
port.addEventListener('message', (e) => console.log('From worker2:', e.data));
port.start();
port.postMessage('Hello from worker1');
});
// Cancel a group of operations atomically
async function fetchWithCleanup(urls, { signal } = {}) {
const controller = new AbortController();
// Cancel our internal controller if the parent signal fires
signal?.addEventListener('abort', () => controller.abort(signal.reason), {
once: true,
signal: controller.signal, // auto-remove listener when we're done
});
try {
return await Promise.all(
urls.map(url => fetch(url, { signal: controller.signal }))
);
} catch (err) {
controller.abort(err); // cancel remaining on first failure
throw err;
}
}
// Race with cleanup: cancel losers when winner is found
async function raceWithCleanup(operations) {
const controller = new AbortController();
try {
return await Promise.any(
operations.map(op => op({ signal: controller.signal }))
);
} finally {
controller.abort(); // cancel all remaining operations
}
}
// Browser
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
event.preventDefault(); // prevent default browser behavior (console error)
});
window.addEventListener('rejectionhandled', (event) => {
console.log('Previously unhandled rejection was handled:', event.promise);
});
// Node.js
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
process.exit(1); // crash — unknown state is dangerous
});
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
process.exit(1);
});
// Node.js — enable async stack traces (slight performance cost)
// node --async-stack-traces script.js (default in Node 12+)
// The Error.captureStackTrace pattern
class AsyncError extends Error {
constructor(message, options) {
super(message, options);
this.name = 'AsyncError';
// Stack trace starts here, not inside Error constructor
Error.captureStackTrace?.(this, AsyncError);
}
}
// Preserving context across async boundaries with cause
async function loadConfig(path) {
try {
const text = await readFile(path, 'utf8');
return JSON.parse(text);
} catch (err) {
throw new Error(`Failed to load config from ${path}`, { cause: err });
}
}
// BAD: async void — fire and forget, errors are silently swallowed
someButton.addEventListener('click', async () => {
await doSomething(); // if this throws, nobody knows
});
// GOOD: handle the error
someButton.addEventListener('click', () => {
doSomething().catch(err => showErrorToUser(err));
});
// BAD: floating promise (not awaited, not .catch'd)
function processItem(item) {
fetch('/api/track'); // fire and forget — may fail silently
return transformItem(item);
}
// BAD: Promise constructor wrapping a Promise
const p = new Promise(resolve => resolve(fetch(url))); // unnecessary wrapping
// BAD: sequential await where parallel is possible
const a = await getA();
const b = await getB(); // waits unnecessarily
const c = await getC(); // waits unnecessarily
// GOOD
const [a, b, c] = await Promise.all([getA(), getB(), getC()]);
// BAD: .then() inside async function (mixing styles)
async function getData() {
return fetch(url).then(r => r.json()); // confusing mix
}
// GOOD: consistent async/await
async function getData() {
const res = await fetch(url);
return res.json();
}