| name | typescript-async-patterns |
| user-invocable | false |
| description | Use when typeScript async patterns including Promises, async/await, and async iterators with proper typing. Use when writing asynchronous TypeScript code. |
| allowed-tools | ["Bash","Read","Write","Edit"] |
TypeScript Async Patterns
Master asynchronous programming patterns in TypeScript, including Promises,
async/await, error handling, async iterators, and advanced patterns for
building robust async applications.
Promises and async/await
Basic Promise Creation
function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function fetchUserData(userId: string): Promise<User> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId) {
resolve({ id: userId, name: 'John Doe' });
} else {
reject(new Error('Invalid user ID'));
}
}, 1000);
});
}
fetchUserData('123')
.then((user) => {
console.log(user.name);
})
.catch((error) => {
console.error('Error:', error.message);
});
async/await Syntax
interface User {
id: string;
name: string;
email: string;
}
interface Post {
id: string;
userId: string;
title: string;
content: string;
}
async function getUserPosts(userId: string): Promise<Post[]> {
try {
const user = await fetchUserData(userId);
const posts = await fetchPostsByUser(user.id);
return posts;
} catch (error) {
console.error('Failed to fetch user posts:', error);
throw error;
}
}
const getUserProfile = async (userId: string): Promise<User> => {
const user = await fetchUserData(userId);
return user;
};
() {
posts = ();
.();
}
Type-Safe Promise Wrappers
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function safeAsync<T>(
promise: Promise<T>
): Promise<Result<T>> {
try {
const data = await promise;
return { success: true, data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error(String(error)),
};
}
}
async function example() {
const result = await safeAsync(fetchUserData('123'));
if (result.success) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}
}
Promise Chaining and Composition
Chaining Promises
interface ApiResponse<T> {
data: T;
status: number;
}
function fetchData<T>(url: string): Promise<ApiResponse<T>> {
return fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => ({
data,
status: 200,
}));
}
function processUserData(userId: string): Promise<string> {
return fetchUserData(userId)
.then((user) => fetchPostsByUser(user.id))
.then((posts) => posts.filter((post) => post.title.includes()))
.( )
.( {
.(, error);
;
});
}
Composing Async Functions
type AsyncFunction<T, R> = (input: T) => Promise<R>;
function pipe<T, A, B>(
fn1: AsyncFunction<T, A>,
fn2: AsyncFunction<A, B>
): AsyncFunction<T, B> {
return async (input: T) => {
const result1 = await fn1(input);
return fn2(result1);
};
}
const getUserId = async (username: string): Promise<string> => {
return '123';
};
const getUserData = async (userId: string): Promise<User> => {
return fetchUserData(userId);
};
const getUserByUsername = pipe(getUserId, getUserData);
const user = await getUserByUsername('johndoe');
Error Handling in Async Code
Try-Catch with async/await
class ApiError extends Error {
constructor(
message: string,
public statusCode: number,
public response?: unknown
) {
super(message);
this.name = 'ApiError';
}
}
async function fetchWithErrorHandling<T>(url: string): Promise<T> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new ApiError(
`HTTP error! status: ${response.status}`,
response.status
);
}
const data = await response.json();
return data;
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error ${error.statusCode}: ${error.message}`);
} else if (error instanceof ) {
.(, error.);
} {
.(, error);
}
error;
}
}
Error Recovery Patterns
async function fetchWithFallback<T>(
primaryUrl: string,
fallbackUrl: string
): Promise<T> {
try {
return await fetchWithErrorHandling<T>(primaryUrl);
} catch (error) {
console.warn('Primary fetch failed, trying fallback');
return await fetchWithErrorHandling<T>(fallbackUrl);
}
}
async function fetchWithMultipleFallbacks<T>(
urls: string[]
): Promise<T> {
let lastError: Error | undefined;
for (const url of urls) {
try {
return await fetchWithErrorHandling<T>(url);
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
console.warn(`Failed to fetch from ${url}, trying next...`);
}
}
throw new Error(
`All fetches failed. Last error: ${lastError?.message ?? 'Unknown'}`
);
}
Typed Error Handling
class ValidationError extends Error {
constructor(message: string, public field: string) {
super(message);
this.name = 'ValidationError';
}
}
class NetworkError extends Error {
constructor(message: string, public url: string) {
super(message);
this.name = 'NetworkError';
}
}
type AppError = ValidationError | NetworkError | Error;
async function handleUserUpdate(userId: string, data: unknown): Promise<void> {
try {
await validateUserData(data);
await updateUser(userId, data);
} catch (error) {
if (error instanceof ) {
.();
} (error ) {
.();
} (error ) {
.();
}
error;
}
}
Promise Combinators
Promise.all
interface UserData {
profile: User;
posts: Post[];
comments: Comment[];
}
async function fetchUserDataParallel(userId: string): Promise<UserData> {
const [profile, posts, comments] = await Promise.all([
fetchUserData(userId),
fetchPostsByUser(userId),
fetchCommentsByUser(userId),
]);
return { profile, posts, comments };
}
async function fetchMultipleResources() {
const [users, posts, settings] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchSettings(),
] as const);
const firstUser: User = users[0];
const firstPost: Post = posts[];
}
Promise.allSettled
interface SettledResult<T> {
status: 'fulfilled' | 'rejected';
value?: T;
reason?: Error;
}
async function fetchAllUserData(
userIds: string[]
): Promise<Array<SettledResult<User>>> {
const results = await Promise.allSettled(
userIds.map((id) => fetchUserData(id))
);
return results.map((result) => {
if (result.status === 'fulfilled') {
return { status: 'fulfilled', value: result.value };
} else {
return { status: 'rejected', reason: result.reason };
}
});
}
const results = await fetchAllUserData(['1', '2', '3']);
const successful = results.filter( r. === );
failed = results.( r. === );
.();
Promise.race and Promise.any
async function fetchWithTimeout<T>(
promise: Promise<T>,
timeoutMs: number
): Promise<T> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Operation timed out')), timeoutMs);
});
return Promise.race([promise, timeout]);
}
const data = await fetchWithTimeout(fetchUserData('123'), 5000);
async function fetchFromFastestServer<T>(
urls: string[]
): Promise<T> {
const fetchPromises = urls.map((url) => fetchWithErrorHandling<T>(url));
try {
return await Promise.any(fetchPromises);
} catch (error) {
throw new Error('All servers failed to respond');
}
}
Async Iterators and Generators
Basic Async Iterators
interface AsyncIteratorResult<T> {
value: T;
done: boolean;
}
async function* numberGenerator(max: number): AsyncGenerator<number> {
for (let i = 0; i < max; i++) {
await delay(100);
yield i;
}
}
async function consumeNumbers() {
for await (const num of numberGenerator(5)) {
console.log(num);
}
}
Async Iterable Data Streams
interface DataChunk {
data: string;
timestamp: number;
}
class AsyncDataStream implements AsyncIterable<DataChunk> {
constructor(private source: string[]) {}
async *[Symbol.asyncIterator](): AsyncGenerator<DataChunk> {
for (const data of this.source) {
await delay(100);
yield {
data,
timestamp: Date.now(),
};
}
}
}
async function processStream() {
const stream = new AsyncDataStream(['chunk1', 'chunk2', 'chunk3']);
for await (const chunk of stream) {
console.log(`Received at ${chunk.timestamp}: `);
}
}
Transforming Async Iterables
async function* mapAsync<T, R>(
iterable: AsyncIterable<T>,
mapper: (value: T) => Promise<R> | R
): AsyncGenerator<R> {
for await (const value of iterable) {
yield await mapper(value);
}
}
async function* filterAsync<T>(
iterable: AsyncIterable<T>,
predicate: (value: T) => Promise<boolean> | boolean
): AsyncGenerator<T> {
for await (const value of iterable) {
if (await predicate(value)) {
yield value;
}
}
}
async function transformData() {
const stream = new AsyncDataStream(['1', '2', '3', '4', '5']);
const numbers = mapAsync(stream, () => (chunk.));
evenNumbers = (numbers, n % === );
( num evenNumbers) {
.(num);
}
}
Observable Patterns
Simple Observable Implementation
type Observer<T> = (value: T) => void;
type Unsubscribe = () => void;
class Observable<T> {
private observers: Set<Observer<T>> = new Set();
subscribe(observer: Observer<T>): Unsubscribe {
this.observers.add(observer);
return () => {
this.observers.delete(observer);
};
}
next(value: T): void {
this.observers.forEach((observer) => observer(value));
}
pipe<R>(operator: (obs: Observable<T>) => Observable<R>): Observable<R> {
return operator(this);
}
}
function map<T, R>(mapper: R) {
(: <T>): <R> => {
result = <R>();
source.( {
result.((value));
});
result;
};
}
numbers = <>();
doubled = numbers.(( n * ));
doubled.( .(value));
numbers.();
Async Observable
interface AsyncObserver<T> {
next: (value: T) => Promise<void> | void;
error?: (error: Error) => Promise<void> | void;
complete?: () => Promise<void> | void;
}
class AsyncObservable<T> {
private observers: Set<AsyncObserver<T>> = new Set();
subscribe(observer: AsyncObserver<T>): Unsubscribe {
this.observers.add(observer);
return () => {
this.observers.delete(observer);
};
}
async next(value: T): Promise<void> {
await Promise.all(
Array.from(this.).( obs.(value))
);
}
(: ): <> {
.(
.(.)
.( obs.)
.( obs.!(error))
);
}
(): <> {
.(
.(.)
.( obs.)
.( obs.!())
);
}
}
Cancellation Patterns
AbortController for Cancellation
async function fetchWithCancellation(
url: string,
signal: AbortSignal
): Promise<Response> {
const response = await fetch(url, { signal });
if (signal.aborted) {
throw new Error('Request was cancelled');
}
return response;
}
const controller = new AbortController();
const fetchPromise = fetchWithCancellation(
'https://api.example.com/data',
controller.signal
);
setTimeout(() => controller.abort(), 5000);
try {
const response = await fetchPromise;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.log('Request was cancelled');
}
}
Cancellable Promise Wrapper
interface CancellablePromise<T> extends Promise<T> {
cancel: () => void;
}
function makeCancellable<T>(
promise: Promise<T>
): CancellablePromise<T> {
let cancelled = false;
const wrappedPromise = new Promise<T>((resolve, reject) => {
promise
.then((value) => {
if (!cancelled) {
resolve(value);
}
})
.catch((error) => {
if (!cancelled) {
reject(error);
}
});
}) as CancellablePromise<T>;
wrappedPromise.cancel = () => {
cancelled = true;
};
return wrappedPromise;
}
const cancellable = makeCancellable(fetchUserData('123'));
setTimeout(() => {
cancellable.cancel();
}, 1000);
Retry and Timeout Patterns
Retry with Exponential Backoff
interface RetryOptions {
maxAttempts: number;
baseDelay: number;
maxDelay: number;
backoffMultiplier: number;
}
async function retry<T>(
fn: () => Promise<T>,
options: RetryOptions
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt < options.maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < options.maxAttempts - 1) {
const delayMs = Math.min(
options.baseDelay * Math.pow(options.backoffMultiplier, attempt),
options.maxDelay
);
console.log(`Attempt failed, retrying in ms`);
(delayMs);
}
}
}
(
);
}
data = (
(),
{
: ,
: ,
: ,
: ,
}
);
Conditional Retry
type RetryPredicate = (error: Error, attempt: number) => boolean;
async function retryWhen<T>(
fn: () => Promise<T>,
shouldRetry: RetryPredicate,
maxAttempts: number
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxAttempts - 1 && shouldRetry(lastError, attempt)) {
await delay(1000 * (attempt + 1));
} else {
throw lastError;
}
}
}
throw lastError!;
}
const data = await retryWhen(
() => (),
error ,
);
Timeout Pattern
class TimeoutError extends Error {
constructor(message: string) {
super(message);
this.name = 'TimeoutError';
}
}
async function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
message = 'Operation timed out'
): Promise<T> {
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => {
reject(new TimeoutError(message));
}, timeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
clearTimeout(timeoutId!);
}
}
async function retryWithTimeout<T>(
fn: () => Promise<T>,
options: RetryOptions & { : }
): <T> {
(
((), options.),
options
);
}
Async Type Inference
Inferring Promise Types
type Awaited<T> = T extends Promise<infer U> ? U : T;
type UserPromise = Promise<User>;
type UserType = Awaited<UserPromise>;
type ReturnTypeAsync<T extends (...args: any) => any> =
Awaited<ReturnType<T>>;
async function getUser(): Promise<User> {
return { id: '1', name: 'John', email: 'john@example.com' };
}
type UserFromFunction = ReturnTypeAsync<typeof getUser>;
Typed Async Function Utilities
type AsyncFn<Args extends any[], R> = (...args: Args) => Promise<R>;
function composeAsync<A, B, C>(
f: AsyncFn<[A], B>,
g: AsyncFn<[B], C>
): AsyncFn<[A], C> {
return async (a: A) => {
const b = await f(a);
return g(b);
};
}
function memoizeAsync<Args extends any[], R>(
fn: AsyncFn<Args, R>
): AsyncFn<Args, R> {
const cache = new Map<string, Promise<R>>();
return async (...args: Args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const promise = (...args);
cache.(key, promise);
{
promise;
} (error) {
cache.(key);
error;
}
};
}
Best Practices
-
Always Handle Errors: Use try-catch with async/await or .catch() with
Promises. Never let errors go unhandled in async code.
-
Avoid Mixing Paradigms: Choose either Promise chains or async/await
for a given flow. Mixing them makes code harder to read and maintain.
-
Use Promise.all for Parallel Operations: When operations are
independent, use Promise.all to run them in parallel rather than
sequentially.
-
Type Promise Return Values: Always explicitly type the return value
of async functions and Promises for better type safety and IDE support.
-
Handle Race Conditions: Be careful with shared state in async code.
Use proper synchronization or immutable data structures.
-
Set Timeouts for Network Requests: Always add timeouts to prevent
hanging requests. Use AbortController or Promise.race.
-
Implement Proper Cleanup: Use finally blocks or try/finally to ensure
cleanup code runs regardless of success or failure.
-
Avoid Async in Constructors: Constructors cannot be async. Use
factory functions or initialization methods instead.
-
Use AbortController for Cancellation: Prefer standard AbortController
over custom cancellation for better browser/Node.js compatibility.
-
Document Async Behavior: Clearly document what async functions do,
what they return, and what errors they might throw.
Common Pitfalls
-
Forgetting await: Forgetting await on async functions returns a
Promise instead of the resolved value, causing type errors and bugs.
-
Sequential When Parallel Is Better: Using await in loops when
operations could run in parallel leads to poor performance.
-
Unhandled Promise Rejections: Not catching errors in Promises or
async functions can crash Node.js applications or cause silent failures.
-
Floating Promises: Not awaiting or handling Promises
(fire-and-forget) can cause unhandled rejections and race conditions.
-
Promise Constructor Anti-pattern: Wrapping already-promisified
functions in new Promise is unnecessary and adds complexity.
-
Async IIFE Mistakes: Forgetting to await or handle errors from
immediately-invoked async functions causes silent failures.
-
Wrong Error Type Assumptions: Assuming all errors are Error
instances. Use proper type checking or type guards.
-
Memory Leaks in Async Iterators: Not properly cleaning up async
iterators can cause memory leaks, especially with infinite streams.
-
Ignoring Cancellation: Not implementing cancellation for long-running
operations wastes resources and degrades user experience.
-
Over-Using Async: Making everything async when synchronous
alternatives exist adds unnecessary complexity and performance overhead.
When to Use This Skill
Use TypeScript async patterns when you need to:
- Make API calls or interact with external services
- Perform I/O operations (file system, database, network)
- Build real-time applications with streaming data
- Handle user interactions that trigger async operations
- Implement background tasks or scheduled jobs
- Work with WebSockets or Server-Sent Events
- Process large datasets asynchronously
- Build responsive UIs that don't block the main thread
- Implement retry logic for unreliable operations
- Create composable async workflows
This skill is essential for full-stack developers, frontend engineers
working with APIs, backend developers building services, and anyone building
modern JavaScript/TypeScript applications.
Resources
Documentation
Books and Articles
- "You Don't Know JS: Async & Performance" by Kyle Simpson
- "JavaScript: The Definitive Guide" by David Flanagan
- "Async JavaScript" by Trevor Burnham
Libraries
Tools