Technology · TypeScript
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.
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; } }
Tip: Use Promise.all for independent operations and sequential awaits for dependent ones — it makes a huge performance difference.
Warning: Always handle errors in async functions — unhandled rejections can crash your application.