TypeScript Async Patterns
A quick reference for async/await, Promises, error handling, and concurrent operations in TypeScript.
TL;DR
- 01Use async/await to write asynchronous code that reads synchronously.
- 02Always wrap async calls in try/catch to handle Promise rejections.
- 03Use Promise.all to run independent async operations in parallel.
Tips
- 01Use Promise.all for independent operations and sequential awaits for dependent ones — it makes a huge performance difference.
Warnings
- 01Always handle errors in async functions — unhandled rejections can crash your application.
Async/Await Basics
- Use async/await for readable asynchronous code.
async function fetchUser(id: number): Promise<User> { const response = await fetch(`/api/users/${id}`); return response.json(); } - Await pauses execution until the Promise resolves.
async function loadData() { const data = await fetchUser(1); // waits here console.log(data.name); // runs after fetch completes } - Async functions always return a Promise automatically.
async function getValue(): Promise<number> { return 42; // wrapped in Promise automatically } - Chain multiple awaits for sequential dependent calls.
async function loadProfile(userId: number) { const user = await fetchUser(userId); const posts = await fetchPosts(user.id); return { user, posts }; } - Mark top-level module code as async when needed.
// top-level await in modules const config = await loadConfig(); console.log(config.apiUrl);
Error Handling
- Handle errors with try/catch blocks.
async function fetchData() { try { const data = await fetch('/api/data'); return data.json(); } catch (error) { console.error('Failed to fetch:', error); return null; } } - Catch both fetch and JSON parsing errors in one block.
async function safeLoad(url: string) { try { const res = await fetch(url); const json = await res.json(); // can also throw return json; } catch (err) { return null; } } - Use a typed catch block to inspect the error shape.
async function fetchUser(id: number) { try { return await getUser(id); } catch (err) { if (err instanceof Error) { console.error(err.message); } } } - Re-throw after logging to propagate to the caller.
async function loadConfig() { try { return await fetchConfig(); } catch (err) { logger.error('Config load failed', err); throw err; // let the caller decide } } - Handle HTTP errors separately from network errors.
async function apiGet(url: string) { const res = await fetch(url); if (!res.ok) { throw new Error(`HTTP ${res.status}: ${res.statusText}`); } return res.json(); }
Concurrent Operations
- Run multiple async operations in parallel with Promise.all.
async function loadAllData() { const [users, posts, comments] = await Promise.all([ fetchUsers(), fetchPosts(), fetchComments() ]); return { users, posts, comments }; } - Promise.all is faster than sequential awaits for independent calls.
// Slow: sequential (waits each time) const user = await fetchUser(1); const posts = await fetchPosts(1); // Fast: parallel (runs at same time) const [user, posts] = await Promise.all([fetchUser(1), fetchPosts(1)]); - Use Promise.allSettled to collect all results even on failure.
const results = await Promise.allSettled([ fetchUser(1), fetchUser(2), fetchUser(3) ]); results.forEach(r => { if (r.status === "fulfilled") console.log(r.value); else console.error(r.reason); }); - Use Promise.race to get the first resolved result.
const fastest = await Promise.race([ fetchFromRegion("us"), fetchFromRegion("eu") ]); - Use Promise.any to get first fulfilled (ignores rejections).
const first = await Promise.any([ fetchMirror1(), fetchMirror2(), fetchMirror3() ]);
Sequential Operations
- Run operations in sequence when the result depends on the previous call.
async function getPostWithAuthor(postId: number) { const post = await fetchPost(postId); const author = await fetchUser(post.userId); return { post, author }; } - Await each operation in order to preserve dependency.
async function createAndNotify(data: NewUser) { const user = await createUser(data); const token = await generateToken(user.id); await sendWelcomeEmail(user.email, token); return user; } - Loop with await to process items one at a time.
async function processQueue(ids: number[]) { for (const id of ids) { await processItem(id); // each waits before next } } - Use reduce to accumulate sequential async results.
async function runSteps(steps: Array<() => Promise<string>>) { return steps.reduce(async (prev, step) => { const results = await prev; const result = await step(); return [...results, result]; }, Promise.resolve<string[]>([])); } - Use for-await-of to iterate async iterables.
async function readStream(stream: AsyncIterable<string>) { for await (const chunk of stream) { console.log(chunk); } }
Type-Safe Async Operations
- Type Promise return values explicitly for caller safety.
async function getData(): Promise<{ id: number; name: string }> { const response = await fetch('/api/data'); return response.json(); } - Use generics to build reusable typed fetch helpers.
async function fetchData<T>(url: string): Promise<T> { const response = await fetch(url); return response.json() as T; } const user = await fetchData<User>('/api/user/1'); - Type async callbacks passed as arguments.
async function withRetry<T>( fn: () => Promise<T>, retries: number ): Promise<T> { for (let i = 0; i <= retries; i++) { try { return await fn(); } catch (e) { if (i === retries) throw e; } } throw new Error("unreachable"); } - Use Result types to encode success and failure explicitly.
type Result<T> = { ok: true; value: T } | { ok: false; error: string }; async function safeFetch(url: string): Promise<Result<unknown>> { try { const data = await (await fetch(url)).json(); return { ok: true, value: data }; } catch (e) { return { ok: false, error: String(e) }; } } - Annotate async generator functions with their yield types.
async function* paginate<T>( load: (page: number) => Promise<T[]> ): AsyncGenerator<T> { let page = 0; while (true) { const items = await load(page++); if (!items.length) break; for (const item of items) yield item; } }
FAQ
Annotate the return type as Promise
Sequential awaits run one after another, so total time is the sum of each duration. Promise.all launches all operations simultaneously and resolves when the last one finishes, which is far faster when the operations don't depend on each other's results.
It does catch them — but only if you await Promise.all inside the try block. If you store the Promise without awaiting it, the rejection goes unhandled. Always use await Promise.all([...]) directly inside try/catch.
TypeScript types caught errors as unknown, so narrow the type before using it: if (error instanceof Error) { console.log(error.message) }. Avoid casting to any, as that defeats type safety.
Technically yes, but the loop won't wait for each async call to finish — forEach doesn't await the returned Promises. Use a for...of loop with await instead, or Promise.all with .map() if the iterations are independent.