Modern JavaScript applications require sophisticated error handling to ensure reliability and maintainability. Let's explore contemporary approaches to handling errors effectively in JavaScript applications.
Understanding Error Types
Built-in Error Types
JavaScript provides several built-in error types:
// Error Types
const error = new Error('Generic error');
const typeError = new TypeError('Invalid type');
const syntaxError = new SyntaxError('Invalid syntax');
const referenceError = new ReferenceError('Invalid reference');
Custom Error Classes
Create custom error types:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
class APIError extends Error {
constructor(message, status) {
super(message);
this.name = 'APIError';
this.status = status;
}
}
Modern Error Handling Patterns
Async/Await Pattern
Handle errors in async functions:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new APIError('Failed to fetch user', response.status);
}
return await response.json();
} catch (error) {
if (error instanceof APIError) {
console.error(`API Error: ${error.message}`);
} else {
console.error(`Unexpected error: ${error.message}`);
}
throw error;
}
}
Promise Chain Pattern
Handle errors in promise chains:
function processData(data) {
return Promise.resolve(data)
.then(validate)
.then(transform)
.then(save)
.catch(error => {
if (error instanceof ValidationError) {
return handleValidationError(error);
}
throw error;
})
.finally(() => {
cleanup();
});
}
Advanced Error Handling
Error Boundaries
Implement error boundaries for components:
class ErrorBoundary {
constructor() {
this.errors = [];
}
catch(error) {
this.errors.push({
error,
timestamp: new Date(),
handled: false
});
this.handleError(error);
}
handleError(error) {
// Custom error handling logic
}
}
Error Aggregation
Aggregate multiple errors:
class ErrorAggregator {
constructor() {
this.errors = new Map();
}
add(error, context) {
const key = `${error.name}:${context}`;
const count = (this.errors.get(key)?.count || 0) + 1;
this.errors.set(key, {
error,
context,
count,
lastOccurrence: new Date()
});
}
getReport() {
return Array.from(this.errors.values());
}
}
Real-World Examples
API Error Handling
Handle API errors effectively:
async function apiRequest(endpoint, options = {}) {
try {
const response = await fetch(endpoint, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
throw new APIError(
`API request failed: ${response.statusText}`,
response.status
);
}
return await response.json();
} catch (error) {
if (error instanceof APIError) {
if (error.status === 401) {
await refreshToken();
return apiRequest(endpoint, options);
}
}
throw error;
}
}
Form Validation
Handle form validation errors:
class FormValidator {
validate(data) {
const errors = new Map();
for (const [field, value] of Object.entries(data)) {
try {
this.validateField(field, value);
} catch (error) {
errors.set(field, error.message);
}
}
if (errors.size > 0) {
throw new ValidationError('Form validation failed', errors);
}
return true;
}
validateField(field, value) {
// Field-specific validation logic
}
}
Best Practices
Error Logging
Implement comprehensive error logging:
class ErrorLogger {
constructor() {
this.logs = [];
}
log(error, context = {}) {
const entry = {
name: error.name,
message: error.message,
stack: error.stack,
context,
timestamp: new Date()
};
this.logs.push(entry);
this.persist(entry);
}
async persist(entry) {
// Send to logging service
}
}
Recovery Strategies
Implement error recovery:
class ErrorRecovery {
async recover(error) {
switch (error.name) {
case 'NetworkError':
return await this.retryWithBackoff();
case 'ValidationError':
return this.handleValidationError(error);
case 'AuthError':
return await this.refreshAndRetry();
default:
throw error;
}
}
async retryWithBackoff(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
await this.delay(Math.pow(2, attempt) * 1000);
}
}
}
}
Testing Error Handling
Error Testing Patterns
Test error handling code:
describe('Error Handling', () => {
it('should handle validation errors', async () => {
const validator = new FormValidator();
expect(() => {
validator.validate({
email: 'invalid-email'
});
}).toThrow(ValidationError);
});
it('should recover from network errors', async () => {
const recovery = new ErrorRecovery();
const error = new NetworkError('Connection failed');
await expect(recovery.recover(error))
.resolves.toBeDefined();
});
});
Conclusion
Modern error handling in JavaScript requires:
- Proper error classification
- Effective error recovery strategies
- Comprehensive logging
- Thorough testing
- Graceful degradation
Key takeaways:
- Use custom error classes
- Implement proper async error handling
- Add comprehensive logging
- Test error scenarios
- Plan for recovery
Remember that good error handling is crucial for:
- Application reliability
- User experience
- Debugging efficiency
- System maintenance
- Code quality