TypeScript Async/Await — Complete Pattern Guide
In this tutorial, you will learn about TypeScript Async/Await. We cover key concepts, practical examples, and best practices to help you master this topic.
TypeScript async/await with full type inference makes asynchronous code as readable as synchronous code while ensuring that promise rejections, parallel execution, and concurrency patterns are all type-checked at compile time.
What You'll Learn
- Async function types and generics
- Parallel vs sequential execution
- Typed error handling in async code
- Async iterators and generators
- Promise pooling and concurrency limits
- AbortController for cancellation
Why It Matters
JavaScript's single-threaded event loop makes non-blocking code essential. Without TypeScript, async code is prone to type errors — a promise resolving with User but consumed as Admin, or forgetting to handle a rejection path. TypeScript ensures the entire async flow is type-safe.
Real-World Use
The DodaZIP cloud sync service processes thousands of concurrent file uploads using typed async patterns. Each upload is a tracked promise with progress, cancellation, and typed error handling — TypeScript ensures the upload pipeline never mixes file metadata types.
Learning Path
flowchart LR A[Error Handling] --> B[Async Patterns] B --> C[Pattern Matching] B --> D[You Are Here] C --> E[Performance] D --> F[Project: REST API]
Typed Async Functions
Every async function returns a Promise<T>. TypeScript infers the resolved type:
// TypeScript infers: () => Promise<string>
async function fetchGreeting(): Promise<string> {
return 'Hello, World!';
}
// Explicit generic — useful for complex types
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json(); // Inferred as User because of return type
}
Expected output:
const greeting: string = await fetchGreeting();
const user: User = await fetchUser('123');
Parallel vs Sequential Execution
Understanding when to await sequentially vs in parallel is critical for performance:
// ❌ Sequential — slow
async function loadDataSequential() {
const start = Date.now();
const user = await fetchUser('123');
const posts = await fetchPosts('123');
const notifications = await fetchNotifications('123');
console.log(`Sequential took ${Date.now() - start}ms`);
return { user, posts, notifications };
}
// ✅ Parallel — fast when operations are independent
async function loadDataParallel() {
const start = Date.now();
const [user, posts, notifications] = await Promise.all([
fetchUser('123'),
fetchPosts('123'),
fetchNotifications('123'),
]);
console.log(`Parallel took ${Date.now() - start}ms`);
return { user, posts, notifications };
}
Why it matters: Sequential requests to independent APIs waste time waiting. Promise.all runs them concurrently, completing when the slowest operation finishes.
Type Safety with Promise.all
TypeScript infers the tuple type from Promise.all:
const result = await Promise.all([
fetchUser('123'), // Promise<User>
fetchPosts('123'), // Promise<Post[]>
fetchNotifications(), // Promise<Notification[]>
]);
// result is typed as [User, Post[], Notification[]]
const user = result[0]; // User
const posts = result[1]; // Post[]
Async Iterators and Generators
Async generators produce sequences of values over time — useful for paginated APIs and streams:
interface Page<T> {
items: T[];
nextCursor?: string;
}
async function* paginateUsers(cursor?: string): AsyncGenerator<User[], void, undefined> {
do {
const page = await fetchPageOfUsers(cursor);
yield page.items;
cursor = page.nextCursor;
} while (cursor);
}
// Usage — for-await-of consumes the generator
async function processAllUsers() {
let total = 0;
for await (const users of paginateUsers()) {
total += users.length;
console.log(`Processed ${users.length} users (total: ${total})`);
}
}
Async Iterator for Stream Processing
class DataStream<T> implements AsyncIterable<T> {
constructor(private data: T[], private delayMs = 100) {}
[Symbol.asyncIterator](): AsyncIterator<T> {
let index = 0;
const data = this.data;
const delay = this.delayMs;
return {
async next(): Promise<IteratorResult<T>> {
if (index >= data.length) {
return { done: true, value: undefined as unknown as T };
}
await new Promise((r) => setTimeout(r, delay));
return { done: false, value: data[index++] };
},
};
}
}
// Usage
const stream = new DataStream([1, 2, 3, 4, 5], 200);
for await (const value of stream) {
console.log(value); // Logs 1, 2, 3, 4, 5 with 200ms intervals
}
Concurrency Control with Promise Pooling
Running 1,000 promises simultaneously can overwhelm system resources. Limit concurrency:
async function promisePool<T>(
items: T[],
concurrency: number,
handler: (item: T) => Promise<void>
): Promise<void> {
const queue = [...items];
const workers: Promise<void>[] = [];
for (let i = 0; i < concurrency; i++) {
workers.push(worker());
}
async function worker(): Promise<void> {
while (queue.length > 0) {
const item = queue.shift()!;
await handler(item);
}
}
await Promise.all(workers);
}
// Process 100 files with 5 concurrent uploads
const files = Array.from({ length: 100 }, (_, i) => `file_${i}.txt`);
await promisePool(files, 5, async (file) => {
console.log(`Uploading ${file}...`);
await uploadFile(file);
console.log(`Uploaded ${file}`);
});
Generic Promise Pool with Results
async function promisePoolAll<T, R>(
items: T[],
concurrency: number,
handler: (item: T) => Promise<R>
): Promise<R[]> {
const results: R[] = [];
const queue = [...items.entries()]; // [index, item]
const workers = Array.from({ length: Math.min(concurrency, items.length) }, worker);
async function worker(): Promise<void> {
while (queue.length > 0) {
const [index, item] = queue.shift()!;
results[index] = await handler(item);
}
}
await Promise.all(workers);
return results;
}
// Usage
const results = await promisePoolAll(files, 5, async (file) => {
return uploadFile(file); // Each upload returns a result
});
// results is typed as R[] matching the return type of uploadFile
AbortController for Cancellation
TypeScript has full types for AbortController and AbortSignal:
async function fetchWithTimeout<T>(
url: string,
timeoutMs = 5000
): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
} finally {
clearTimeout(timeoutId);
}
}
// Advanced: cancellable operation with status tracking
interface CancellablePromise<T> {
promise: Promise<T>;
cancel: () => void;
}
function cancellable<T>(
executor: (signal: AbortSignal) => Promise<T>
): CancellablePromise<T> {
const controller = new AbortController();
const promise = executor(controller.signal).finally(() => {
// Cleanup if needed
});
return {
promise,
cancel: () => controller.abort(),
};
}
// Usage
const { promise, cancel } = cancellable(async (signal) => {
const response = await fetch('/api/large-file', { signal });
return response.json();
});
// Cancel if it takes too long
setTimeout(() => cancel(), 3000);
const data = await promise; // Throws if cancelled
Async Queue Pattern
Process tasks with backpressure:
class AsyncQueue<T> {
private queue: T[] = [];
private resolvers: ((value: T) => void)[] = [];
push(item: T): void {
if (this.resolvers.length > 0) {
const resolve = this.resolvers.shift()!;
resolve(item);
} else {
this.queue.push(item);
}
}
async pop(): Promise<T> {
if (this.queue.length > 0) {
return this.queue.shift()!;
}
return new Promise((resolve) => {
this.resolvers.push(resolve);
});
}
get length(): number {
return this.queue.length;
}
}
// Producer-consumer pattern
const queue = new AsyncQueue<string>();
// Producer
async function producer() {
for (let i = 0; i < 10; i++) {
await new Promise((r) => setTimeout(r, 100));
queue.push(`Task ${i}`);
}
}
// Consumer
async function consumer() {
for (let i = 0; i < 10; i++) {
const task = await queue.pop();
console.log(`Processing: ${task}`);
}
}
await Promise.all([producer(), consumer()]);
Common Mistakes
1. Sequential Promise.all when independent awaited
Awaiting promises sequentially instead of with Promise.all adds unnecessary latency to operations that could run in parallel.
2. Forgetting to handle rejections
An unhandled promise rejection crashes the process in Node.js. Always add .catch() or use try-catch with async/await.
3. Not using AbortController for fetch timeouts
Fetch requests without timeouts can hang indefinitely. Always pair fetch with AbortController and a timeout.
4. Running too many promises at once
1,000 concurrent requests overwhelm APIs and system resources. Use promise pooling to limit concurrency.
5. Ignoring async generator cleanup
Async generators that acquire resources (file handles, database connections) must clean up in finally blocks or return handlers.
6. Overusing async when not needed
Synchronous operations wrapped in async create unnecessary microtasks. Only use async for I/O operations.
7. Not typing async function return values
Omitting the return type on async functions hides the resolved type. Always annotate Promise<T> with the expected type.
Practice Questions
What's the difference between
Promise.allandPromise.allSettled?Promise.allrejects immediately if any promise rejects.Promise.allSettledwaits for all promises and returns their results regardless of rejection.How do you cancel an async operation in TypeScript? Use
AbortController— passAbortSignalto fetch or custom async operations, and callcontroller.abort()to cancel.What does an async generator function return? An
AsyncGenerator<T, TReturn, TNext>object that implementsAsyncIterable<T>and can be consumed withfor-await-of.How do you type the resolved value of Promise.all with an array? TypeScript infers the tuple type from an array literal in
Promise.all. For dynamic arrays, usePromise.all<T>(items)with a generic.What's backpressure and why does it matter? Backpressure means controlling the rate of data flow so producers don't overwhelm consumers. Async queues with bounded buffers implement backpressure.
Challenge
Build a concurrent file processor that reads 1,000 files with a concurrency limit of 10, processes each file through a pipeline (read → transform → compress → upload), supports cancellation via AbortController, and tracks progress with typed status updates.
FAQ
Mini Project
Build a concurrent web scraper:
- Async generator: Paginate through 50 API pages
- Promise pool: Scrape 5 pages concurrently
- AbortController: Cancel if total time exceeds 30 seconds
- Async queue: Buffer scraped items for processing
- Result tracking: Typed results for each page with success/error status
What's Next
You've mastered async patterns with TypeScript. Now explore pattern matching with {{< ref "53-pattern-matching" >}}, or optimize performance with {{< ref "54-performance" >}}.
For a hands-on project, see {{< ref "55-project-rest-api" >}}.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro