withTimeout
Wraps an async function with a timeout, rejecting if execution exceeds the specified duration. Provides a clean, reusable way to add timeout behavior to any async operation using Promise.race. Perfect for preventing hanging operations, implementing SLA enforcement, and ensuring responsive user interfaces. Combines any async function with a timeout mechanism while preserving the original function's return type and error behavior.
Signature
const withTimeout: <T>(fn: AsyncFn<[], T>, ms: number, options?: DelayOptions) => Promise<T>
Parameters
| Name | Type | Description |
|---|---|---|
fn | - | The async function to execute with timeout protection |
ms | - | Maximum execution time in milliseconds before timing out |
options | - | Configuration options for timeout behavior |
Returns
Promise that resolves with function result or rejects with TimeoutError
Examples
Basic API call with timeout
import { withTimeout } from '@winglet/common-utils';
async function fetchUserData(userId: string) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
// Add 5-second timeout to API call
try {
const userData = await withTimeout(
() => fetchUserData('123'),
5000
);
console.log('User data:', userData);
} catch (error) {
if (error instanceof TimeoutError) {
console.error('API call timed out after 5 seconds');
} else {
console.error('API call failed:', error);
}
}
Database operations with timeout
async function createTimeoutWrapper<T>(
operation: () => Promise<T>,
operationName: string,
timeoutMs: number = 10000
): Promise<T> {
try {
return await withTimeout(operation, timeoutMs);
} catch (error) {
if (error instanceof TimeoutError) {
throw new Error(
`${operationName} timed out after ${timeoutMs}ms. Check database connection.`
);
}
throw error;
}
}
// Usage with database operations
const user = await createTimeoutWrapper(
() => db.users.findById(userId),
'User query',
5000
);
const result = await createTimeoutWrapper(
() => db.orders.create(orderData),
'Order creation',
15000
);
File operations with timeout
import { promises as fs } from 'fs';
async function processFileWithTimeout(filePath: string, maxProcessingTime: number) {
const processFile = async () => {
console.log(`Starting to process: ${filePath}`);
// Read file
const content = await fs.readFile(filePath, 'utf-8');
// Simulate processing
const lines = content.split('\n');
const processedLines = lines.map(line => line.trim().toUpperCase());
// Write processed file
const outputPath = filePath.replace('.txt', '_processed.txt');
await fs.writeFile(outputPath, processedLines.join('\n'));
return {
inputPath: filePath,
outputPath,
lineCount: lines.length
};
};
try {
return await withTimeout(processFile, maxProcessingTime);
} catch (error) {
if (error instanceof TimeoutError) {
console.error(`File processing timed out after ${maxProcessingTime}ms`);
throw new Error(`File ${filePath} took too long to process`);
}
throw error;
}
}
// Usage
try {
const result = await processFileWithTimeout('./large-data.txt', 30000);
console.log(`Processed ${result.lineCount} lines -> ${result.outputPath}`);
} catch (error) {
console.error('File processing failed:', error.message);
}
HTTP client with configurable timeouts
class TimeoutHTTPClient {
constructor(
private baseURL: string,
private defaultTimeout: number = 10000
) {}
async get<T>(
endpoint: string,
timeoutMs?: number
): Promise<T> {
const fetchOperation = async () => {
const response = await fetch(`${this.baseURL}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
};
return withTimeout(fetchOperation, timeoutMs ?? this.defaultTimeout);
}
async post<T>(
endpoint: string,
data: any,
timeoutMs?: number
): Promise<T> {
const postOperation = async () => {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
};
return withTimeout(postOperation, timeoutMs ?? this.defaultTimeout);
}
}
// Usage
const client = new TimeoutHTTPClient('https://api.example.com', 5000);
try {
// Use default 5-second timeout
const users = await client.get<User[]>('/users');
// Use custom 15-second timeout for slow endpoint
const report = await client.get<Report>('/analytics/heavy-report', 15000);
// Post with timeout
const newUser = await client.post<User>('/users', userData, 8000);
} catch (error) {
if (error instanceof TimeoutError) {
console.error('Request timed out');
} else {
console.error('Request failed:', error.message);
}
}
Cancellable operations with AbortSignal
async function cancellableDataProcessing(
data: any[],
progressCallback?: (progress: number) => void
) {
const controller = new AbortController();
// Allow user to cancel operation
const cancelButton = document.getElementById('cancel-btn');
cancelButton?.addEventListener('click', () => {
console.log('User requested cancellation');
controller.abort();
});
const processData = async () => {
const results = [];
for (let i = 0; i < data.length; i++) {
// Check for cancellation
if (controller.signal.aborted) {
throw new AbortError('USER_CANCELLED', 'Processing cancelled by user');
}
// Process item (simulate work)
const processed = await processItem(data[i]);
results.push(processed);
// Report progress
const progress = ((i + 1) / data.length) * 100;
progressCallback?.(progress);
}
return results;
};
try {
// Process with 5-minute timeout
const results = await withTimeout(
processData,
300000,
{ signal: controller.signal }
);
console.log('Processing completed successfully');
return results;
} catch (error) {
if (error instanceof TimeoutError) {
console.error('Processing timed out after 5 minutes');
} else if (error instanceof AbortError) {
console.log('Processing was cancelled');
} else {
console.error('Processing failed:', error);
}
throw error;
}
}
Service health check with timeout
interface ServiceHealth {
service: string;
status: 'healthy' | 'unhealthy' | 'timeout';
responseTime?: number;
error?: string;
}
async function checkServiceHealth(
serviceName: string,
healthcheckUrl: string,
timeoutMs: number = 5000
): Promise<ServiceHealth> {
const startTime = Date.now();
const healthCheck = async () => {
const response = await fetch(healthcheckUrl);
if (!response.ok) {
throw new Error(`Health check failed: HTTP ${response.status}`);
}
return response.json();
};
try {
await withTimeout(healthCheck, timeoutMs);
return {
service: serviceName,
status: 'healthy',
responseTime: Date.now() - startTime
};
} catch (error) {
if (error instanceof TimeoutError) {
return {
service: serviceName,
status: 'timeout',
responseTime: Date.now() - startTime,
error: `Timeout after ${timeoutMs}ms`
};
} else {
return {
service: serviceName,
status: 'unhealthy',
responseTime: Date.now() - startTime,
error: error.message
};
}
}
}
// Usage - check multiple services
const services = [
{ name: 'User Service', url: 'https://users.api.com/health' },
{ name: 'Order Service', url: 'https://orders.api.com/health' },
{ name: 'Payment Service', url: 'https://payments.api.com/health' }
];
const healthChecks = await Promise.all(
services.map(service =>
checkServiceHealth(service.name, service.url, 3000)
)
);
healthChecks.forEach(health => {
console.log(`${health.service}: ${health.status} (${health.responseTime}ms)`);
});
Testing async operations with timeout
describe('Async operations with timeout', () => {
it('should complete within time limit', async () => {
const fastOperation = async () => {
await delay(100);
return 'completed';
};
const result = await withTimeout(fastOperation, 1000);
expect(result).toBe('completed');
});
it('should timeout for slow operations', async () => {
const slowOperation = async () => {
await delay(2000);
return 'completed';
};
await expect(
withTimeout(slowOperation, 500)
).rejects.toThrow('Timeout after 500ms');
});
it('should propagate original errors', async () => {
const failingOperation = async () => {
throw new Error('Operation failed');
};
await expect(
withTimeout(failingOperation, 1000)
).rejects.toThrow('Operation failed');
});
});
Playground
import { withTimeout } from '@winglet/common-utils'; async function fetchUserData(userId: string) { const response = await fetch(`/api/users/${userId}`); return response.json(); } // Add 5-second timeout to API call try { const userData = await withTimeout( () => fetchUserData('123'), 5000 ); console.log('User data:', userData); } catch (error) { if (error instanceof TimeoutError) { console.error('API call timed out after 5 seconds'); } else { console.error('API call failed:', error); } }
Notes
Key Features:
- Type Preservation: Maintains original function's return type and signature
- Error Transparency: Original function errors are propagated unchanged
- Race Condition: Uses Promise.race for efficient timeout implementation
- Cancellation Support: Full AbortSignal integration for both function and timeout
- Zero Overhead: No additional processing when function completes within timeout
Execution Flow:
- Start both the original function and timeout concurrently via Promise.race
- If function completes first, return its result (timeout is automatically cleaned up)
- If timeout occurs first, throw TimeoutError (function may continue running)
- If AbortSignal is triggered, both function and timeout are cancelled
Error Hierarchy:
- Function Errors: Propagated as-is, preserving original error types
- TimeoutError: Thrown when execution time exceeds limit
- AbortError: Thrown when cancelled via AbortSignal
Common Use Cases:
- API Calls: Prevent hanging network requests
- Database Operations: Avoid blocking on slow queries
- File I/O: Limit time spent on large file operations
- User Interactions: Set maximum wait times for user input
- Service Health Checks: Quick determination of service availability
- Background Processing: Prevent runaway background tasks
Best Practices:
- Set timeout values based on expected operation duration plus buffer
- Handle TimeoutError specifically to provide meaningful user feedback
- Use AbortSignal for operations that should be user-cancellable
- Consider retry logic for operations that might succeed on subsequent attempts
- Log timeout occurrences for monitoring and performance optimization
Performance Considerations:
- Memory Usage: ~100-150 bytes per withTimeout call (Promise.race + timeout overhead)
- CPU Overhead: <0.3ms for Promise.race setup and coordination
- Concurrency: Efficiently handles thousands of concurrent withTimeout operations
- React 18 Compatibility:
- Concurrent Features: Safe with React.startTransition and useDeferredValue
- Suspense: Works correctly with React.Suspense boundaries
- Automatic Batching: Timeout errors won't break React's automatic batching
- useEffect Cleanup: Pairs perfectly with AbortController in useEffect
- Bundle Size: ~200 bytes minified + gzipped (excluding dependencies)
- Browser Optimization: Promise.race is highly optimized in modern engines
- Node.js Performance: No GC pressure from efficient Promise handling