Handling Timeouts in Async Operations
Timeout handling prevents operations from hanging indefinitely.
Promise.race Pattern
function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
errorMessage: string = 'Operation timed out'
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(errorMessage)), timeoutMs);
});
return Promise.race([promise, timeoutPromise]);
}
Usage
try {
const result = await withTimeout(
fetchData(),
5000,
'Fetch request timed out after 5 seconds'
);
} catch (error) {
if (error.message === 'Fetch request timed out after 5 seconds') {
console.log('Request timed out');
} else {
console.log('Other error:', error);
}
}
AbortController Pattern
For fetch requests and other abortable operations:
async function fetchWithTimeout(
url: string,
options: RequestInit = {},
timeoutMs: number = 30000
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw error;
}
}
Custom Timeout Error
class TimeoutError extends Error {
constructor(message: string, public readonly timeoutMs: number) {
super(message);
this.name = 'TimeoutError';
}
}
function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number
): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new TimeoutError(
`Operation timed out after ${timeoutMs}ms`,
timeoutMs
)), timeoutMs)
),
]);
}
Timeout with Cleanup
async function withTimeoutAndCleanup<T>(
operation: () => Promise<T>,
timeoutMs: number,
cleanup: () => void | Promise<void>
): Promise<T> {
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => {
cleanup(); // Run cleanup on timeout
reject(new Error(`Operation timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
try {
return await Promise.race([operation(), timeoutPromise]);
} finally {
clearTimeout(timeoutId);
}
}
Configurable Timeout Manager
class TimeoutManager {
private activeTimeouts: Map<string, NodeJS.Timeout> = new Map();
set(key: string, callback: () => void, delayMs: number): void {
this.clear(key); // Clear existing timeout if any
const timeoutId = setTimeout(() => {
callback();
this.activeTimeouts.delete(key);
}, delayMs);
this.activeTimeouts.set(key, timeoutId);
}
clear(key: string): void {
const timeoutId = this.activeTimeouts.get(key);
if (timeoutId) {
clearTimeout(timeoutId);
this.activeTimeouts.delete(key);
}
}
clearAll(): void {
this.activeTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));
this.activeTimeouts.clear();
}
}
Progressive Timeout
async function withProgressiveTimeout<T>(
operation: () => Promise<T>,
timeouts: number[] // [1000, 2000, 5000, 10000]
): Promise<T> {
for (let i = 0; i < timeouts.length; i++) {
try {
return await withTimeout(operation(), timeouts[i]);
} catch (error) {
if (i === timeouts.length - 1) {
throw error; // Re-throw on last attempt
}
console.log(`Timeout ${i + 1}/${timeouts.length} (${timeouts[i]}ms), retrying...`);
}
}
throw new Error('All timeouts failed');
}
Timeout with Fallback
async function withTimeoutAndFallback<T>(
operation: () => Promise<T>,
timeoutMs: number,
fallback: () => T
): Promise<T> {
try {
return await withTimeout(operation(), timeoutMs);
} catch (error) {
if (error.message.includes('timed out')) {
console.log('Operation timed out, using fallback');
return fallback();
}
throw error;
}
}
For API Clients
class ApiClient {
constructor(
private baseUrl: string,
private defaultTimeoutMs: number = 30000,
private maxRetries: number = 3
) {}
async get<T>(
endpoint: string,
timeoutMs?: number
): Promise<T> {
const timeout = timeoutMs ?? this.defaultTimeoutMs;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await withTimeout(
fetch(`${this.baseUrl}${endpoint}`).then(r => r.json()),
timeout
);
} catch (error) {
if (attempt === this.maxRetries || !error.message.includes('timed out')) {
throw error;
}
// Retry with exponential backoff
await sleep(Math.pow(2, attempt) * 1000);
}
}
throw new Error('All attempts failed');
}
}
Common Timeout Values
| Operation | Recommended Timeout |
|---|---|
| HTTP request | 30 seconds |
| Database query | 10 seconds |
| File read | 5 seconds |
| AI generation | 120 seconds |
| Image processing | 30 seconds |
| Video render | 600 seconds |
Best Practices
- Always set timeouts for external operations
- Use descriptive error messages including timeout duration
- Clean up resources on timeout (connections, file handles)
- Log timeout events for monitoring
- Configure timeouts based on operation type
- Consider progressive timeouts for retries
- Use AbortController for fetch requests