본문으로 건너뛰기

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

NameTypeDescription
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:

  1. Start both the original function and timeout concurrently via Promise.race
  2. If function completes first, return its result (timeout is automatically cleaned up)
  3. If timeout occurs first, throw TimeoutError (function may continue running)
  4. 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