TypeScript Async Patterns
Promise Combinators
// Parallel — all must succeed
const [user, posts, settings] = await Promise.all([
fetchUser(id),
fetchPosts(id),
fetchSettings(id),
])
// Parallel — some may fail (inspects results individually)
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()])
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value)
else console.error(r.reason)
})
// Race — first to resolve wins
const data = await Promise.race([fetchPrimary(), fetchFallback()])
// Any — first to succeed (ignores rejections unless all fail)
const fastest = await Promise.any([mirror1(), mirror2(), mirror3()])
AbortController & Timeout
// Fetch with timeout
async function fetchWithTimeout<T>(url: string, ms = 5000): Promise<T> {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), ms)
try {
const res = await fetch(url, { signal: controller.signal })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json() as Promise<T>
} finally {
clearTimeout(timer)
}
}
// Native timeout (Node 18+ / browsers)
const res = await fetch(url, { signal: AbortSignal.timeout(5000) })
// Cancel multiple operations
const controller = new AbortController()
const { signal } = controller
await Promise.all([
fetch('/api/a', { signal }),
fetch('/api/b', { signal }),
])
// Cancel all on user action
button.addEventListener('click', () => controller.abort())
Async Retry with Backoff
async function withRetry<T>(
fn: () => Promise<T>,
{ retries = 3, delay = 200, factor = 2 }: RetryOptions = {}
): Promise<T> {
let lastError: unknown
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await fn()
} catch (err) {
lastError = err
if (attempt < retries) {
await new Promise(r => setTimeout(r, delay * factor ** attempt))
}
}
}
throw lastError
}
const data = await withRetry(() => fetchUser(id), { retries: 3, delay: 500 })
Async Queue / Rate Limiter
class AsyncQueue {
private queue: Array<() => Promise<unknown>> = []
private running = 0
constructor(private concurrency: number) {}
async add<T>(task: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try { resolve(await task()) } catch (e) { reject(e) }
finally { this.running--; this.next() }
})
this.next()
})
}
private next() {
while (this.running < this.concurrency && this.queue.length) {
this.running++
this.queue.shift()!()
}
}
}
// Usage: process up to 5 items in parallel
const queue = new AsyncQueue(5)
await Promise.all(items.map(item => queue.add(() => processItem(item))))
Async Iterators & Generators
// Async generator — paginated API
async function* paginate<T>(
fetcher: (cursor?: string) => Promise<{ items: T[]; next?: string }>
) {
let cursor: string | undefined
do {
const { items, next } = await fetcher(cursor)
yield* items
cursor = next
} while (cursor)
}
// Consume
for await (const user of paginate(cursor => api.users.list({ cursor }))) {
await processUser(user)
}
// Readable stream as async iterator (Node 16+)
import { createReadStream } from 'node:fs'
const stream = createReadStream('large.csv', 'utf8')
for await (const chunk of stream) {
process(chunk.toString())
}
Deferred / Promise Handle
function createDeferred<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 }
}
// Usage: resolve from outside
const { promise, resolve } = createDeferred<string>()
setTimeout(() => resolve('done'), 1000)
const result = await promise
Async Error Patterns
// Never lose errors in fire-and-forget
function fireAndForget(promise: Promise<unknown>) {
promise.catch(err => console.error('Unhandled async error:', err))
}
// Result type to avoid try/catch everywhere
async function safe<T>(fn: () => Promise<T>): Promise<[T, null] | [null, Error]> {
try {
return [await fn(), null]
} catch (e) {
return [null, e instanceof Error ? e : new Error(String(e))]
}
}
const [user, err] = await safe(() => fetchUser(id))
if (err) return handleError(err)
Event Emitter → Promise
import { once } from 'node:events'
// Wait for a single event
const [data] = await once(emitter, 'data')
// With timeout
const timeout = AbortSignal.timeout(5000)
const [data] = await once(emitter, 'data', { signal: timeout })