TypeScript Async Patterns
Type async/await, Promise chains, and concurrent patterns correctly in TypeScript
When to Use
- Typing async functions and their return values
- Handling errors in async code with proper types
- Running concurrent operations with Promise.all, Promise.allSettled, Promise.race
- Creating typed async utilities and middleware
Instructions
- Async function return types — always return
Promise<T>:
async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
- Type Promise.all — preserves tuple types:
const [user, posts, settings] = await Promise.all([
getUser(userId), // Promise<User>
getUserPosts(userId), // Promise<Post[]>
getSettings(), // Promise<Settings>
]);
// Types: [User, Post[], Settings]
- Promise.allSettled — returns settled results with status:
const results = await Promise.allSettled([fetchUser(), fetchPosts()]);
for (const result of results) {
if (result.status === 'fulfilled') {
console.log(result.value); // Type: User | Post[] (union of all types)
} else {
console.log(result.reason); // Type: any
}
}
- Promise.race — returns the type of the first resolved:
const result = await Promise.race([
fetchData(), // Promise<Data>
timeout(5000), // Promise<never>
]);
// Type: Data (never is absorbed)
- Typed error handling — catch blocks receive
unknown:
async function fetchUser(id: string): Promise<User> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch user: ${error.message}`);
}
throw error;
}
}
- Result type pattern — avoid exceptions for expected failures:
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
async function safeFetch<T>(url: string): Promise<Result<T>> {
try {
const res = await fetch(url);
if (!res.ok) return { ok: false, error: new Error(`HTTP ${res.status}`) };
return { ok: true, value: await res.json() };
} catch (error) {
return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
}
}
- Typed retry logic:
async function retry<T>(
fn: () => Promise<T>,
options: { retries: number; delay: number }
): Promise<T> {
for (let i = 0; i <= options.retries; i++) {
try {
return await fn();
} catch (error) {
if (i === options.retries) throw error;
await new Promise((r) => setTimeout(r, options.delay * (i + 1)));
}
}
throw new Error('Unreachable');
}
- Concurrent execution with concurrency limit:
async function mapConcurrent<T, R>(
items: T[],
fn: (item: T) => Promise<R>,
concurrency: number
): Promise<R[]> {
const results: R[] = [];
const executing = new Set<Promise<void>>();
for (const item of items) {
const p = fn(item).then((result) => {
results.push(result);
});
executing.add(p);
p.finally(() => executing.delete(p));
if (executing.size >= concurrency) {
await Promise.race(executing);
}
}
await Promise.all(executing);
return results;
}
- AsyncIterable for streaming data:
async function* paginate<T>(
fetchPage: (cursor?: string) => Promise<{ data: T[]; nextCursor?: string }>
): AsyncGenerator<T> {
let cursor: string | undefined;
do {
const page = await fetchPage(cursor);
yield* page.data;
cursor = page.nextCursor;
} while (cursor);
}
for await (const user of paginate(fetchUsers)) {
console.log(user);
}
Details
TypeScript types async code through the Promise<T> generic type. The await keyword unwraps Promise<T> to T, and async functions always return Promise<T>.
Awaited<T> utility type: Recursively unwraps nested Promises. Awaited<Promise<Promise<string>>> is string. Useful for typing the result of Promise.all and similar utilities.
Error typing limitations: catch blocks and .catch() callbacks receive unknown (with useUnknownInCatchVariables). TypeScript cannot track which errors a function might throw. This is by design — any function can throw any error. Use the Result pattern for typed error handling.
void vs undefined in async: An async function with no return statement returns Promise<void>, not Promise<undefined>. These are subtly different — void means "ignore the return value," undefined means "the value is undefined."
Unhandled rejection risks: Forgetting to await a Promise means errors are unhandled. TypeScript does not warn about unawaited promises by default — use the @typescript-eslint/no-floating-promises rule.
Trade-offs:
Promise.allfails fast — if one promise rejects, the entire result rejects. UsePromise.allSettledwhen partial success is acceptable- Async generators are powerful but hard to debug and have poor error propagation
- The Result pattern avoids exceptions but adds verbosity — use it for expected failures (API errors), not unexpected ones (null references)
Source
https://typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.