TypeScript Async Patterns
A developer guide to async/await, Promise types, error handling, and concurrency patterns in TypeScript.
TL;DR
- Annotate asynchronous function returns explicitly with the generic
Promise<T>type. - Narrow caught error instances with
instanceof Errorbefore reading properties. - Execute parallel asynchronous operations safely using the
Promise.allutility.
Async Function Signatures
Promise return typeWraps the resolved value type inside a generic Promise.
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}async arrow functionApplies asynchronous execution to arrow expressions.
const loadData = async (): Promise<number[]> => {
return [10, 20, 30];
};top-level awaitAwaits promises directly at the module root level.
// Allowed in ES modules
const config = await loadRemoteConfig();
console.log(config.endpoint);Error Handling Patterns
try/catch with unknownCatches unknown exceptions and narrows using instanceof.
try {
await riskyOperation();
} catch (err: unknown) {
if (err instanceof Error) console.error(err.message);
}custom error guardValidates application-specific error structures safely.
interface ApiError { code: number; message: string; }
function isApiError(e: unknown): e is ApiError {
const isObj = typeof e === 'object' && e !== null;
return isObj && 'code' in e;
}result tuple patternReturns tuples resembling Go-style error handling.
async function safeRun<T>(
p: Promise<T>
): Promise<[T | null, Error | null]> {
try {
return [await p, null];
} catch (e) {
return [null, e as Error];
}
}Concurrent Operations
Promise.allRuns multiple independent tasks in parallel.
const [user, orders] = await Promise.all([
fetchUser('u1'),
fetchOrders('u1')
]);Promise.allSettledWaits for all tasks without failing on single rejections.
const results = await Promise.allSettled([
syncAnalytics(),
syncProfile()
]);
for (const r of results) {
if (r.status === 'fulfilled') console.log(r.value);
}Promise.raceResolves or rejects as soon as the fastest Promise completes.
const data = await Promise.race([
fetchData(),
timeout(5000)
]);Sequential Iteration
for...of await loopExecutes asynchronous iterations sequentially in order.
for (const url of urls) {
const res = await fetch(url);
console.log(await res.text());
}for await...of loopConsumes asynchronous iterable streams item by item.
async function streamData(
stream: AsyncIterable<string>
) {
for await (const chunk of stream) {
console.log(chunk);
}
}sequential reduceChains asynchronous operations sequentially with reduce.
const final = await tasks.reduce(
async (prev, nextTask) => {
await prev;
return nextTask();
},
Promise.resolve()
);Cancellation Patterns
AbortController signalPropagates cancellation signals into async operations.
const controller = new AbortController();
const res = await fetch('/api/data', {
signal: controller.signal
});
// controller.abort(); // cancels in-flight requesttimeout with AbortSignalAutomatically aborts network operations after deadlines.
const timeoutSignal = AbortSignal.timeout(3000);
const response = await fetch('/api/heavy', {
signal: timeoutSignal
});checking signal abortedChecks cancellation state in long-running loops.
function runWork(signal: AbortSignal) {
if (signal.aborted) throw new Error('Aborted');
}Tips
- Type caught error variables as
unknownand use type guards before reading.messageor.stackattributes. - Use
Promise.allSettled()instead ofPromise.all()when individual task rejections should not abort the entire batch.
Warnings
- Never use
asynccallbacks directly inside array.forEach()loops because promises are not awaited sequentially. - Forgetting to await a rejected
Promiseinsidetry/catchcauses unhandled rejections that crash Node.js processes.
In Practice
Demonstrates typed Promise batches, concurrency with Promise.allSettled, and strict error handling.
- Declare a list of entity identifiers to process concurrently.
- Launch parallel network calls using Promise.allSettled.
- Iterate over results inspecting the discriminated status tag.
- Separate fulfilled values from rejected errors safely.
interface User { id: string; }
async function fetchUser(id: string): Promise<User> {
if (id === 'err') throw new Error(id);
return { id };
}
async function sync(ids: string[]) {
const tasks = ids.map(fetchUser);
const res = await Promise.allSettled(tasks);
const ok = res.filter(r => r.status === 'fulfilled');
console.log(`Completed: ${ok.length}/${ids.length}`);
}
sync(['1', 'err', '2']);FAQ
TypeScript defaults catch clause bindings to unknown under strict mode. Rather than casting the variable to any, use a type guard like if (err instanceof Error) before accessing .message or .stack to guarantee runtime safety.
Array.prototype.forEach executes callbacks synchronously without awaiting the returned Promises, causing subsequent lines to run before your async tasks complete. Instead, iterate with for...of loops or map over the array with Promise.all().
for (const item of items) {
await processItem(item);
}Promise.all() rejects immediately as soon as any single Promise in the array rejects, discarding other in-flight results. Promise.allSettled() waits for every Promise to finish and returns an array of discriminated outcome objects indicating whether each item fulfilled or rejected.
Annotate async generator functions using the built-in AsyncGenerator generic interface. For generators that only yield items without returning a final value, you can type the return signature simply as AsyncIterable.