
typescript-async-patterns
by knopki
There's no place like $HOME
SKILL.md
name: typescript-async-patterns description: Use when typeScript async patterns including Promises, async/await, and async iterators with proper typing. Use when writing asynchronous TypeScript code.
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
// 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);
});
async/await Syntax
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-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)),
};
}
}
// Usage
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,
}));
}
// 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";
});
}
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);
};
}
// 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");
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 TypeError) {
console.error("Network error:", error.message);
} else {
console.error("Unknown error:", error);
}
throw 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);
}
}
// 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"}`,
);
}
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 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;
}
}
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 };
}
// 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];
}
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 };
}
});
}
// 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 and Promise.any
// 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");
}
}
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;
}
}
// Using async iterator
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(),
};
}
}
}
// 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}`);
}
}
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;
}
}
}
// 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
}
}
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);
}
}
// 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
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.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!()),
);
}
}
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;
}
// 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");
}
}
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;
}
// Usage
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 ${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,
});
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!;
}
// Usage: Only retry on network errors
const data = await retryWhen(
() => fetchUserData("123"),
(error) => error instanceof NetworkError,
3,
);
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!);
}
}
// Combined retry with timeout
async function retryWithTimeout<T>(
fn: () => Promise<T>,
options: RetryOptions & { timeout: number },
): Promise<T> {
return retry(() => withTimeout(fn(), options.timeout), options);
}
Async Type Inference
Inferring Promise Types
// 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
Typed Async Function Utilities
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;
}
};
}
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
- MDN Web Docs - Async/Await: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
- TypeScript Handbook - Async Functions: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-7.html
- JavaScript Promises - MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
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
- RxJS: https://rxjs.dev/ - Reactive Extensions for JavaScript
- p-limit: https://github.com/sindresorhus/p-limit - Promise concurrency control
- p-retry: https://github.com/sindresorhus/p-retry - Retry failed promises
- abort-controller: https://www.npmjs.com/package/abort-controller - AbortController polyfill
Tools
- TypeScript Playground: https://www.typescriptlang.org/play
- Chrome DevTools - Async Stack Traces
- Node.js --trace-warnings flag for debugging unhandled rejections
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon


