📚 4 min read
Error Handling ​
Learn how to effectively handle errors in asynchronous JavaScript code.
Custom Error Types ​
Create specific error types for better error handling:
typescript
class NetworkError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly response?: Response
) {
super(message);
this.name = 'NetworkError';
// Restore prototype chain
Object.setPrototypeOf(this, NetworkError.prototype);
}
}
class ValidationError extends Error {
constructor(
message: string,
public readonly errors: Record<string, string[]>
) {
super(message);
this.name = 'ValidationError';
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
class TimeoutError extends Error {
constructor(message = 'Operation timed out') {
super(message);
this.name = 'TimeoutError';
Object.setPrototypeOf(this, TimeoutError.prototype);
}
}
Error Boundaries ​
Implement error boundaries for async operations:
typescript
class AsyncBoundary {
private errorHandler: (error: Error) => void;
private retryCount: number;
private retryDelay: number;
constructor(options: {
onError: (error: Error) => void;
retryCount?: number;
retryDelay?: number;
}) {
this.errorHandler = options.onError;
this.retryCount = options.retryCount ?? 3;
this.retryDelay = options.retryDelay ?? 1000;
}
async wrap<T>(operation: () => Promise<T>): Promise<T | undefined> {
let lastError: Error;
for (let attempt = 0; attempt < this.retryCount; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
this.errorHandler(lastError);
if (attempt < this.retryCount - 1) {
await new Promise(resolve =>
setTimeout(resolve, this.retryDelay * Math.pow(2, attempt))
);
}
}
}
throw lastError!;
}
}
// Usage
const boundary = new AsyncBoundary({
onError: error => {
console.error('Operation failed:', error);
notifyUser('An error occurred, retrying...');
},
retryCount: 3,
retryDelay: 1000
});
try {
const data = await boundary.wrap(async () => {
const response = await fetch('/api/data');
if (!response.ok) {
throw new NetworkError(
'Failed to fetch data',
response.status,
response
);
}
return response.json();
});
} catch (error) {
if (error instanceof NetworkError) {
handleNetworkError(error);
} else {
handleUnexpectedError(error);
}
}
Error Recovery ​
Implement graceful error recovery:
typescript
async function withFallback<T>(
primary: () => Promise<T>,
fallback: () => Promise<T>,
shouldTryFallback: (error: Error) => boolean
): Promise<T> {
try {
return await primary();
} catch (error) {
if (error instanceof Error && shouldTryFallback(error)) {
return await fallback();
}
throw error;
}
}
// Usage
const data = await withFallback(
() => fetchFromPrimaryAPI(),
() => fetchFromBackupAPI(),
error => error instanceof NetworkError
);
Circuit Breaker ​
Implement a circuit breaker pattern:
typescript
class CircuitBreaker {
private failures = 0;
private lastFailure: number = 0;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private threshold: number = 5,
private timeout: number = 60000,
private onStateChange?: (state: 'closed' | 'open' | 'half-open') => void
) {}
private setState(state: 'closed' | 'open' | 'half-open'): void {
this.state = state;
this.onStateChange?.(state);
}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailure >= this.timeout) {
this.setState('half-open');
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await operation();
if (this.state === 'half-open') {
this.setState('closed');
this.failures = 0;
}
return result;
} catch (error) {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.setState('open');
}
throw error;
}
}
reset(): void {
this.failures = 0;
this.setState('closed');
}
}
// Usage
const breaker = new CircuitBreaker(5, 60000, state => {
console.log(`Circuit breaker state changed to: ${state}`);
});
async function fetchWithBreaker() {
try {
return await breaker.execute(async () => {
const response = await fetch('/api/data');
if (!response.ok) {
throw new NetworkError(
'Failed to fetch data',
response.status,
response
);
}
return response.json();
});
} catch (error) {
if (error.message === 'Circuit breaker is open') {
return await fetchFromFallbackService();
}
throw error;
}
}
Error Aggregation ​
Handle multiple errors gracefully:
typescript
class AggregateError extends Error {
constructor(
message: string,
public readonly errors: Error[]
) {
super(message);
this.name = 'AggregateError';
Object.setPrototypeOf(this, AggregateError.prototype);
}
}
async function executeAll<T>(
tasks: (() => Promise<T>)[],
options = { continueOnError: false }
): Promise<T[]> {
const results: T[] = [];
const errors: Error[] = [];
for (const task of tasks) {
try {
results.push(await task());
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
if (!options.continueOnError) {
throw new AggregateError(
'Task execution failed',
errors
);
}
}
}
if (errors.length > 0) {
throw new AggregateError(
'Some tasks failed to execute',
errors
);
}
return results;
}
// Usage
try {
const results = await executeAll(
[task1, task2, task3],
{ continueOnError: true }
);
} catch (error) {
if (error instanceof AggregateError) {
error.errors.forEach(handleIndividualError);
} else {
handleUnexpectedError(error);
}
}
Best Practices ​
Error Types
- Create specific error classes
- Include relevant error details
- Maintain proper prototype chain
Error Recovery
- Implement retry mechanisms
- Provide fallback options
- Use circuit breakers for unstable services
Error Boundaries
- Contain error propagation
- Implement proper cleanup
- Log errors appropriately
Error Context
- Include relevant error details
- Maintain error stack traces
- Add debugging information
User Experience
- Show meaningful error messages
- Provide recovery options
- Maintain application state