Async/Await Error Handling - Complete Guide
Published: September 25, 2024 | Reading time: 20 minutes
Async/Await Error Handling Overview
Proper error handling in async/await code ensures robust applications:
Error Handling Benefits
# Error Handling Benefits
- Graceful error recovery
- Better user experience
- Debugging capabilities
- Application stability
- Error logging
- Monitoring integration
- Code maintainability
Basic Error Handling Patterns
Try-Catch with Async/Await
Basic Error Handling
# Basic Error Handling Patterns
# 1. Simple Try-Catch
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
return userData;
} catch (error) {
console.error('Error fetching user data:', error);
throw error;
}
}
# 2. Error Handling with Fallback
async function fetchUserDataWithFallback(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const userData = await response.json();
return userData;
} catch (error) {
console.error('Error fetching user data:', error);
// Return fallback data
return {
id: userId,
name: 'Unknown User',
email: 'unknown@example.com'
};
}
}
# 3. Multiple Async Operations
async function fetchUserAndPosts(userId) {
try {
const [userResponse, postsResponse] = await Promise.all([
fetch(`/api/users/${userId}`),
fetch(`/api/users/${userId}/posts`)
]);
if (!userResponse.ok || !postsResponse.ok) {
throw new Error('Failed to fetch user or posts');
}
const [user, posts] = await Promise.all([
userResponse.json(),
postsResponse.json()
]);
return { user, posts };
} catch (error) {
console.error('Error fetching user and posts:', error);
throw error;
}
}
# 4. Sequential Operations with Error Handling
async function processUserData(userId) {
try {
const user = await fetchUserData(userId);
const posts = await fetchUserPosts(user.id);
const comments = await fetchUserComments(user.id);
return {
user,
posts,
comments
};
} catch (error) {
console.error('Error processing user data:', error);
throw error;
}
}
# 5. Error Handling with Timeout
async function fetchWithTimeout(url, timeout = 5000) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
# 6. Retry Logic with Error Handling
async function fetchWithRetry(url, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
lastError = error;
console.log(`Attempt ${attempt} failed:`, error.message);
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}
# 7. Error Handling with Validation
async function createUser(userData) {
try {
// Validate input
if (!userData.email || !userData.name) {
throw new Error('Email and name are required');
}
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to create user');
}
return await response.json();
} catch (error) {
console.error('Error creating user:', error);
throw error;
}
}
# 8. Error Handling with Cleanup
async function processFile(filePath) {
let fileHandle;
try {
fileHandle = await fs.open(filePath, 'r');
const content = await fileHandle.readFile('utf8');
// Process content
const processedContent = await processContent(content);
return processedContent;
} catch (error) {
console.error('Error processing file:', error);
throw error;
} finally {
if (fileHandle) {
await fileHandle.close();
}
}
}
# 9. Error Handling with Logging
async function apiCall(endpoint, data) {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`API call failed: ${response.status}`);
}
const result = await response.json();
// Log success
console.log(`API call successful: ${endpoint}`);
return result;
} catch (error) {
// Log error with context
console.error(`API call failed: ${endpoint}`, {
error: error.message,
data,
timestamp: new Date().toISOString()
});
throw error;
}
}
# 10. Error Handling with Custom Error Types
class APIError extends Error {
constructor(message, status, endpoint) {
super(message);
this.name = 'APIError';
this.status = status;
this.endpoint = endpoint;
}
}
async function fetchData(endpoint) {
try {
const response = await fetch(endpoint);
if (!response.ok) {
throw new APIError(
`Request failed with status ${response.status}`,
response.status,
endpoint
);
}
return await response.json();
} catch (error) {
if (error instanceof APIError) {
console.error(`API Error: ${error.endpoint} - ${error.message}`);
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
Advanced Error Handling Patterns
Complex Error Scenarios
Advanced Error Handling
# Advanced Error Handling Patterns
# 1. Error Boundary Pattern
class AsyncErrorBoundary {
constructor() {
this.errorHandlers = new Map();
}
registerHandler(errorType, handler) {
this.errorHandlers.set(errorType, handler);
}
async execute(operation) {
try {
return await operation();
} catch (error) {
const handler = this.errorHandlers.get(error.constructor);
if (handler) {
return await handler(error);
}
throw error;
}
}
}
# 2. Circuit Breaker Pattern
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.threshold = threshold;
this.timeout = timeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
}
}
}
# 3. Error Aggregation Pattern
async function executeMultipleOperations(operations) {
const results = [];
const errors = [];
for (const operation of operations) {
try {
const result = await operation();
results.push({ success: true, data: result });
} catch (error) {
errors.push(error);
results.push({ success: false, error: error.message });
}
}
if (errors.length > 0) {
console.error('Multiple operations failed:', errors);
}
return results;
}
# 4. Error Recovery Pattern
async function resilientOperation(operation, fallback) {
try {
return await operation();
} catch (error) {
console.error('Primary operation failed:', error);
try {
console.log('Attempting fallback operation...');
return await fallback();
} catch (fallbackError) {
console.error('Fallback operation also failed:', fallbackError);
throw new Error(`Both primary and fallback operations failed: ${error.message}, ${fallbackError.message}`);
}
}
}
# 5. Error Classification Pattern
class ErrorClassifier {
static classify(error) {
if (error.name === 'TypeError') {
return 'VALIDATION_ERROR';
} else if (error.message.includes('timeout')) {
return 'TIMEOUT_ERROR';
} else if (error.message.includes('network')) {
return 'NETWORK_ERROR';
} else if (error.message.includes('permission')) {
return 'PERMISSION_ERROR';
} else {
return 'UNKNOWN_ERROR';
}
}
static async handleError(error) {
const errorType = this.classify(error);
switch (errorType) {
case 'VALIDATION_ERROR':
return { retry: false, message: 'Invalid input provided' };
case 'TIMEOUT_ERROR':
return { retry: true, message: 'Operation timed out' };
case 'NETWORK_ERROR':
return { retry: true, message: 'Network connection failed' };
case 'PERMISSION_ERROR':
return { retry: false, message: 'Insufficient permissions' };
default:
return { retry: false, message: 'An unexpected error occurred' };
}
}
}
# 6. Error Context Pattern
class ErrorContext {
constructor() {
this.context = new Map();
}
set(key, value) {
this.context.set(key, value);
}
get(key) {
return this.context.get(key);
}
async execute(operation) {
try {
return await operation();
} catch (error) {
// Add context to error
error.context = Object.fromEntries(this.context);
throw error;
}
}
}
# 7. Error Monitoring Pattern
class ErrorMonitor {
constructor() {
this.errors = [];
this.maxErrors = 100;
}
logError(error, context = {}) {
const errorEntry = {
timestamp: new Date().toISOString(),
message: error.message,
stack: error.stack,
context
};
this.errors.push(errorEntry);
if (this.errors.length > this.maxErrors) {
this.errors.shift();
}
// Send to monitoring service
this.sendToMonitoring(errorEntry);
}
sendToMonitoring(errorEntry) {
// Implementation depends on monitoring service
console.log('Sending error to monitoring:', errorEntry);
}
getErrorStats() {
const errorTypes = {};
this.errors.forEach(error => {
const type = error.message.split(':')[0];
errorTypes[type] = (errorTypes[type] || 0) + 1;
});
return {
totalErrors: this.errors.length,
errorTypes,
recentErrors: this.errors.slice(-10)
};
}
}
# 8. Error Recovery Strategies
class ErrorRecovery {
static async retryWithBackoff(operation, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
static async retryWithExponentialBackoff(operation, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
static async retryWithJitter(operation, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
const baseDelay = 1000;
const jitter = Math.random() * 1000;
const delay = baseDelay * attempt + jitter;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
}
Error Handling Best Practices
Guidelines and Patterns
Best Practices
- Always use try-catch with async/await
- Handle specific error types
- Implement proper logging
- Use error boundaries
- Implement retry logic
- Provide fallback mechanisms
- Monitor error patterns
Common Mistakes
- Forgetting try-catch blocks
- Not handling promise rejections
- Swallowing errors silently
- Poor error messages
- Not implementing retries
- Missing error context
- Inadequate error monitoring
Production Error Handling
Real-world Implementation
Production Error Handling
# Production Error Handling
# 1. Express.js Error Handling
const express = require('express');
const app = express();
// Error handling middleware
app.use(async (error, req, res, next) => {
console.error('Unhandled error:', error);
// Log error to monitoring service
await logError(error, {
url: req.url,
method: req.method,
userAgent: req.get('User-Agent'),
ip: req.ip
});
// Send appropriate response
if (error.name === 'ValidationError') {
res.status(400).json({
error: 'Validation failed',
message: error.message
});
} else if (error.name === 'UnauthorizedError') {
res.status(401).json({
error: 'Unauthorized',
message: 'Authentication required'
});
} else {
res.status(500).json({
error: 'Internal server error',
message: 'Something went wrong'
});
}
});
# 2. Global Error Handler
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Log to monitoring service
logError(reason, {
type: 'unhandledRejection',
promise: promise.toString()
});
});
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// Log to monitoring service
logError(error, {
type: 'uncaughtException'
});
// Graceful shutdown
process.exit(1);
});
# 3. Error Logging Service
class ErrorLoggingService {
constructor() {
this.logs = [];
}
async logError(error, context = {}) {
const errorLog = {
timestamp: new Date().toISOString(),
message: error.message,
stack: error.stack,
name: error.name,
context
};
this.logs.push(errorLog);
// Send to external service
await this.sendToExternalService(errorLog);
}
async sendToExternalService(errorLog) {
try {
await fetch('https://api.logging-service.com/errors', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.LOGGING_API_KEY}`
},
body: JSON.stringify(errorLog)
});
} catch (error) {
console.error('Failed to send error to external service:', error);
}
}
}
# 4. Error Rate Limiting
class ErrorRateLimiter {
constructor(maxErrors = 10, windowMs = 60000) {
this.maxErrors = maxErrors;
this.windowMs = windowMs;
this.errors = [];
}
canLogError() {
const now = Date.now();
this.errors = this.errors.filter(time => now - time < this.windowMs);
return this.errors.length < this.maxErrors;
}
logError() {
this.errors.push(Date.now());
}
}
# 5. Error Alerting
class ErrorAlerting {
constructor() {
this.alertThresholds = {
errorRate: 10, // errors per minute
errorCount: 100 // total errors
};
this.alertsSent = new Set();
}
async checkAndAlert(errorStats) {
const { errorRate, errorCount } = errorStats;
if (errorRate > this.alertThresholds.errorRate) {
await this.sendAlert('HIGH_ERROR_RATE', {
errorRate,
threshold: this.alertThresholds.errorRate
});
}
if (errorCount > this.alertThresholds.errorCount) {
await this.sendAlert('HIGH_ERROR_COUNT', {
errorCount,
threshold: this.alertThresholds.errorCount
});
}
}
async sendAlert(type, data) {
const alertKey = `${type}_${Date.now()}`;
if (this.alertsSent.has(alertKey)) {
return; // Prevent duplicate alerts
}
this.alertsSent.add(alertKey);
// Send alert to monitoring service
console.log(`Alert: ${type}`, data);
}
}
Summary
Async/await error handling involves several key components:
- Basic Patterns: Try-catch blocks, error propagation, and fallback mechanisms
- Advanced Patterns: Error boundaries, circuit breakers, and error classification
- Best Practices: Guidelines and common mistakes to avoid
- Production Implementation: Real-world error handling and monitoring
Need More Help?
Struggling with async/await error handling or need help implementing robust error management? Our JavaScript experts can help you build resilient applications.
Get Error Handling Help