Help us improve
Share bugs, ideas, or general feedback.
From typescript
Provides TypeScript patterns for Promises, async/await, async iterators, error handling, and type-safe wrappers when writing asynchronous code.
npx claudepluginhub thebushidocollective/han --plugin typescriptHow this skill is triggered — by the user, by Claude, or both
Slash command
/typescript:typescript-async-patternsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Master asynchronous programming patterns in TypeScript, including Promises,
Provides patterns for typing async functions, Promise combinators, error handling, retry logic, and concurrency limits in TypeScript.
Implements practical async error handling in TypeScript using fp-ts TaskEither for clean composable pipelines instead of nested try/catch, with fetch API examples.
Provides Python asyncio and async/await patterns for building async web APIs (FastAPI, aiohttp, Sanic), concurrent I/O operations, web scrapers, and real-time apps.
Share bugs, ideas, or general feedback.
Master asynchronous programming patterns in TypeScript, including Promises, async/await, error handling, async iterators, and advanced patterns for building robust async applications.
// Creating a Promise
function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
// Promise with value
function fetchUserData(userId: string): Promise<User> {
return new Promise((resolve, reject) => {
// Simulated API call
setTimeout(() => {
if (userId) {
resolve({ id: userId, name: 'John Doe' });
} else {
reject(new Error('Invalid user ID'));
}
}, 1000);
});
}
// Using the Promise
fetchUserData('123')
.then((user) => {
console.log(user.name);
})
.catch((error) => {
console.error('Error:', error.message);
});
interface User {
id: string;
name: string;
email: string;
}
interface Post {
id: string;
userId: string;
title: string;
content: string;
}
// Async function declaration
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;
}
}
// Async arrow function
const getUserProfile = async (userId: string): Promise<User> => {
const user = await fetchUserData(userId);
return user;
};
// Using async/await
async function main() {
const posts = await getUserPosts('123');
console.log(`Found ${posts.length} posts`);
}
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)),
};
}
}
// Usage
async function example() {
const result = await safeAsync(fetchUserData('123'));
if (result.success) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}
}
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,
}));
}
// Chaining multiple async operations
function processUserData(userId: string): Promise<string> {
return fetchUserData(userId)
.then((user) => fetchPostsByUser(user.id))
.then((posts) => posts.filter((post) => post.title.includes('TypeScript')))
.then((filteredPosts) => `Found ${filteredPosts.length} TypeScript posts`)
.catch((error) => {
console.error('Error in chain:', error);
return 'Failed to process user data';
});
}
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);
};
}
// Usage
const getUserId = async (username: string): Promise<string> => {
// Look up user ID
return '123';
};
const getUserData = async (userId: string): Promise<User> => {
return fetchUserData(userId);
};
const getUserByUsername = pipe(getUserId, getUserData);
// Use the composed function
const user = await getUserByUsername('johndoe');
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 TypeError) {
console.error('Network error:', error.message);
} else {
console.error('Unknown error:', error);
}
throw error;
}
}
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);
}
}
// Multiple fallbacks
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'}`
);
}
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 ValidationError) {
console.error(`Validation error in field ${error.field}: ${error.message}`);
} else if (error instanceof NetworkError) {
console.error(`Network error for ${error.url}: ${error.message}`);
} else if (error instanceof Error) {
console.error(`Unexpected error: ${error.message}`);
}
throw error;
}
}
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 };
}
// Type-safe Promise.all with tuple
async function fetchMultipleResources() {
const [users, posts, settings] = await Promise.all([
fetchUsers(), // Promise<User[]>
fetchPosts(), // Promise<Post[]>
fetchSettings(), // Promise<Settings>
] as const);
// TypeScript infers correct types
const firstUser: User = users[0];
const firstPost: Post = posts[0];
}
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 };
}
});
}
// Usage
const results = await fetchAllUserData(['1', '2', '3']);
const successful = results.filter((r) => r.status === 'fulfilled');
const failed = results.filter((r) => r.status === 'rejected');
console.log(`${successful.length} succeeded, ${failed.length} failed`);
// Promise.race - first to settle (fulfill or reject)
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]);
}
// Usage
const data = await fetchWithTimeout(fetchUserData('123'), 5000);
// Promise.any - first to fulfill (ignores rejections)
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');
}
}
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;
}
}
// Using async iterator
async function consumeNumbers() {
for await (const num of numberGenerator(5)) {
console.log(num);
}
}
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(),
};
}
}
}
// Usage
async function processStream() {
const stream = new AsyncDataStream(['chunk1', 'chunk2', 'chunk3']);
for await (const chunk of stream) {
console.log(`Received at ${chunk.timestamp}: ${chunk.data}`);
}
}
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;
}
}
}
// Usage
async function transformData() {
const stream = new AsyncDataStream(['1', '2', '3', '4', '5']);
const numbers = mapAsync(stream, (chunk) => parseInt(chunk.data));
const evenNumbers = filterAsync(numbers, (n) => n % 2 === 0);
for await (const num of evenNumbers) {
console.log(num); // 2, 4
}
}
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);
}
}
// Operator
function map<T, R>(mapper: (value: T) => R) {
return (source: Observable<T>): Observable<R> => {
const result = new Observable<R>();
source.subscribe((value) => {
result.next(mapper(value));
});
return result;
};
}
// Usage
const numbers = new Observable<number>();
const doubled = numbers.pipe(map((n) => n * 2));
doubled.subscribe((value) => console.log(value));
numbers.next(5); // 10
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.observers).map((obs) => obs.next(value))
);
}
async error(error: Error): Promise<void> {
await Promise.all(
Array.from(this.observers)
.filter((obs) => obs.error)
.map((obs) => obs.error!(error))
);
}
async complete(): Promise<void> {
await Promise.all(
Array.from(this.observers)
.filter((obs) => obs.complete)
.map((obs) => obs.complete!())
);
}
}
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;
}
// Usage
const controller = new AbortController();
// Start fetch
const fetchPromise = fetchWithCancellation(
'https://api.example.com/data',
controller.signal
);
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
const response = await fetchPromise;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.log('Request was cancelled');
}
}
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;
}
// Usage
const cancellable = makeCancellable(fetchUserData('123'));
setTimeout(() => {
cancellable.cancel();
}, 1000);
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 ${attempt + 1} failed, retrying in ${delayMs}ms`);
await delay(delayMs);
}
}
}
throw new Error(
`Failed after ${options.maxAttempts} attempts: ${lastError?.message ?? 'Unknown error'}`
);
}
// Usage
const data = await retry(
() => fetchUserData('123'),
{
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 5000,
backoffMultiplier: 2,
}
);
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!;
}
// Usage: Only retry on network errors
const data = await retryWhen(
() => fetchUserData('123'),
(error) => error instanceof NetworkError,
3
);
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!);
}
}
// Combined retry with timeout
async function retryWithTimeout<T>(
fn: () => Promise<T>,
options: RetryOptions & { timeout: number }
): Promise<T> {
return retry(
() => withTimeout(fn(), options.timeout),
options
);
}
// Extract Promise type
type Awaited<T> = T extends Promise<infer U> ? U : T;
// Usage
type UserPromise = Promise<User>;
type UserType = Awaited<UserPromise>; // User
// For functions
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>; // User
type AsyncFn<Args extends any[], R> = (...args: Args) => Promise<R>;
// Type-safe async pipe
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);
};
}
// Memoize async function
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 = fn(...args);
cache.set(key, promise);
try {
return await promise;
} catch (error) {
cache.delete(key);
throw error;
}
};
}
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.
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.
Use TypeScript async patterns when you need to:
This skill is essential for full-stack developers, frontend engineers working with APIs, backend developers building services, and anyone building modern JavaScript/TypeScript applications.