Error Handling - Exercise 3
The .catch() method handles rejected Promises. It receives the error as an argument and lets you recover from failures or log errors gracefully.
// Basic Promise with catch
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
console.error('Request failed:', error.message);
});
// Catch can return a fallback value
const getData = () => {
return fetch('https://api.example.com/users')
.then(res => res.json())
.catch(error => {
console.error('Using fallback data');
return []; // Return empty array as fallback
});
};
// Chaining after catch continues the chain
Promise.reject(new Error('Failed'))
.catch(err => 'Recovered')
.then(result => console.log(result)); // "Recovered"
With async/await, you wrap your code in a try...catch block. The catch block receives any error thrown or rejected Promise, making async error handling look like synchronous code.
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('Failed to fetch user:', error.message);
return null; // Return fallback value
}
}
// Using finally for cleanup
async function loadData() {
const loader = showLoadingSpinner();
try {
const data = await fetchData();
displayData(data);
} catch (error) {
showErrorMessage(error.message);
} finally {
loader.hide(); // Always runs
}
}
Promise.allSettled() waits for all promises to complete, regardless of whether they resolve or reject. Each result has a status of "fulfilled" or "rejected" with its value or reason.
const promises = [
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
];
const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} succeeded:`, result.value);
} else {
console.log(`Promise ${index} failed:`, result.reason);
}
});
// Filter successful and failed results
const successful = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
console.log(`${successful.length} succeeded, ${failed.length} failed`);
Unhandled promise rejections occur when a rejected Promise has no catch handler. Modern JavaScript environments provide events to catch these globally and prevent silent failures.
// In browsers: listen for unhandledrejection event
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled rejection:', event.reason);
// Prevent default browser behavior (console error)
event.preventDefault();
// Log to your error tracking service
logErrorToService({
type: 'unhandled_rejection',
error: event.reason,
promise: event.promise
});
});
// In Node.js
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise);
console.error('Reason:', reason);
});
// Example that would trigger the handler
Promise.reject(new Error('Oops!')); // No .catch()
window.onerror is a global event handler that catches uncaught JavaScript errors. It receives error details including the message, source file, line number, and column number.
// Set up global error handler
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global error caught:');
console.error('Message:', message);
console.error('Source:', source);
console.error('Line:', lineno, 'Column:', colno);
console.error('Error object:', error);
// Send to error tracking service
reportError({
message,
source,
lineno,
colno,
stack: error?.stack
});
// Return true to prevent default browser error handling
return true;
};
// This error will be caught by window.onerror
function buggyCode() {
undefinedVariable.doSomething(); // ReferenceError
}
The unhandledrejection event fires when a Promise rejects and no rejection handler is attached. This is your safety net for catching Promise errors that slip through.
// Add global handler for unhandled Promise rejections
window.addEventListener('unhandledrejection', function(event) {
// The promise that was rejected
const promise = event.promise;
// The rejection reason (usually an Error)
const reason = event.reason;
console.error('Unhandled Promise Rejection:');
console.error('Reason:', reason);
// Log detailed error info
if (reason instanceof Error) {
console.error('Stack:', reason.stack);
}
// Prevent the default handling (error in console)
event.preventDefault();
});
// Also handle when rejections are later handled
window.addEventListener('rejectionhandled', function(event) {
console.log('A previously unhandled rejection is now handled');
});
Error boundaries are patterns that catch errors in a section of your application without crashing the entire app. They provide fallback UI and allow the rest of the application to continue working.
// Simple error boundary pattern for vanilla JavaScript
class ErrorBoundary {
constructor(element, fallbackContent) {
this.element = element;
this.fallbackContent = fallbackContent;
}
wrap(fn) {
try {
return fn();
} catch (error) {
this.handleError(error);
return null;
}
}
async wrapAsync(fn) {
try {
return await fn();
} catch (error) {
this.handleError(error);
return null;
}
}
handleError(error) {
console.error('Error caught by boundary:', error);
this.element.innerHTML = this.fallbackContent;
this.logError(error);
}
logError(error) {
// Send to error tracking service
fetch('/api/log-error', {
method: 'POST',
body: JSON.stringify({ error: error.message, stack: error.stack })
});
}
}
Logging errors to an external service helps you track and fix issues in production. You should capture the error message, stack trace, user context, and browser information.
// Error logging service
const ErrorLogger = {
endpoint: '/api/errors',
log(error, context = {}) {
const errorData = {
message: error.message,
stack: error.stack,
name: error.name,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent,
...context
};
// Use sendBeacon for reliability (works even during page unload)
if (navigator.sendBeacon) {
navigator.sendBeacon(this.endpoint, JSON.stringify(errorData));
} else {
fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData),
keepalive: true
});
}
}
};
// Usage with try/catch
try {
riskyOperation();
} catch (error) {
ErrorLogger.log(error, { userId: currentUser.id, action: 'checkout' });
}
Fetch only rejects on network failures, not HTTP errors like 404 or 500. You must check response.ok and throw errors for non-success status codes to handle all failure cases properly.
async function fetchWithErrorHandling(url, options = {}) {
try {
const response = await fetch(url, options);
// Check for HTTP errors (fetch does not reject on 4xx/5xx)
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
error.response = response;
throw error;
}
return await response.json();
} catch (error) {
// Network error (no connection, DNS failure, etc.)
if (error.name === 'TypeError') {
console.error('Network error:', error.message);
throw new Error('Unable to connect. Check your internet connection.');
}
// Timeout error
if (error.name === 'AbortError') {
throw new Error('Request timed out. Please try again.');
}
// Re-throw HTTP or other errors
throw error;
}
}
// Usage with timeout
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
fetchWithErrorHandling('/api/data', { signal: controller.signal });
Retry logic automatically attempts failed operations again with delays between attempts. Use exponential backoff to increase wait times and prevent overwhelming servers.
async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Do not retry on certain errors
if (error.status === 401 || error.status === 403) {
throw error; // Auth errors should not retry
}
if (attempt < maxRetries - 1) {
// Exponential backoff: 1s, 2s, 4s, ...
const delay = baseDelay * Math.pow(2, attempt);
console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
// Usage
const data = await retryWithBackoff(
() => fetch('/api/data').then(r => r.json()),
3, // max 3 attempts
1000 // start with 1 second delay
);
AggregateError is used when multiple errors need to be reported together. It is thrown by Promise.any() when all promises reject, and you can create it manually when validating multiple fields.
// AggregateError from Promise.any when all reject
const promises = [
Promise.reject(new Error('First failed')),
Promise.reject(new Error('Second failed')),
Promise.reject(new Error('Third failed'))
];
try {
await Promise.any(promises);
} catch (error) {
if (error instanceof AggregateError) {
console.log('All promises failed:');
error.errors.forEach((err, i) => {
console.log(` ${i + 1}: ${err.message}`);
});
}
}
// Creating AggregateError manually for validation
function validateForm(data) {
const errors = [];
if (!data.email) errors.push(new Error('Email is required'));
if (!data.password) errors.push(new Error('Password is required'));
if (data.password?.length < 8) errors.push(new Error('Password too short'));
if (errors.length > 0) {
throw new AggregateError(errors, 'Form validation failed');
}
}
Production error handling should be user friendly, secure, and informative for debugging. Never expose stack traces to users, always log errors, and provide graceful degradation.
// Best practices for production error handling
const ErrorHandler = {
// 1. Use custom error classes for different error types
isOperationalError(error) {
return error.isOperational === true;
},
// 2. Show user friendly messages, log detailed info
handle(error) {
// Log full details for debugging
console.error('[Error]', {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
// Show friendly message to user
const userMessage = this.isOperationalError(error)
? error.message
: 'Something went wrong. Please try again later.';
showNotification(userMessage, 'error');
},
// 3. Set up global handlers
init() {
window.onerror = (msg, src, line, col, err) => {
this.handle(err || new Error(msg));
return true;
};
window.addEventListener('unhandledrejection', event => {
this.handle(event.reason);
event.preventDefault();
});
}
};
// 4. Create operational errors with user messages
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.isOperational = true;
this.statusCode = statusCode;
}
}