Chapter 8: JavaScript Error Handling - Complete Guide to Exception Management
Error handling is a critical aspect of JavaScript development that ensures applications remain stable and provide meaningful feedback when things go wrong. Proper error handling improves user experience, makes debugging easier, and helps maintain application reliability.
Why Error Handling is Essential in JavaScript
Error handling in JavaScript is crucial because it:
- Prevents Application Crashes: Gracefully handles unexpected situations
- Improves User Experience: Provides meaningful error messages and recovery options
- Facilitates Debugging: Makes it easier to identify and fix issues
- Enhances Reliability: Ensures applications continue functioning despite errors
- Supports Monitoring: Enables error tracking and analytics
- Enables Graceful Degradation: Allows applications to continue with reduced functionality
Learning Objectives
Through this chapter, you will master:
- Basic error handling with try-catch blocks
- Creating and throwing custom errors
- Error types and their appropriate usage
- Asynchronous error handling
- Error boundaries and recovery strategies
- Debugging techniques and tools
- Best practices for production applications
Basic Error Handling
Try-Catch Blocks
// Basic try-catch structure
try {
// Code that might throw an error
const result = riskyOperation();
console.log('Success:', result);
} catch (error) {
// Handle the error
console.error('Error occurred:', error.message);
} finally {
// Code that always runs
console.log('Cleanup completed');
}
// Example with potential errors
function divideNumbers(a, b) {
try {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Both arguments must be numbers');
}
if (b === 0) {
throw new Error('Division by zero is not allowed');
}
return a / b;
} catch (error) {
console.error('Division error:', error.message);
return null;
}
}
// Usage
console.log(divideNumbers(10, 2)); // 5
console.log(divideNumbers(10, 0)); // null (with error logged)
console.log(divideNumbers('10', 2)); // null (with error logged)
Error Object Properties
function demonstrateErrorProperties() {
try {
// Simulate an error
JSON.parse('invalid json');
} catch (error) {
console.log('Error name:', error.name);
console.log('Error message:', error.message);
console.log('Error stack:', error.stack);
console.log('Error constructor:', error.constructor.name);
// Additional properties
console.log('Error toString():', error.toString());
// Check if it's a specific error type
if (error instanceof SyntaxError) {
console.log('This is a syntax error');
}
}
}
demonstrateErrorProperties();
Finally Block
function processFile(filename) {
let fileHandle = null;
try {
// Simulate opening a file
fileHandle = { name: filename, isOpen: true };
console.log(`Opening file: ${filename}`);
// Simulate file processing
if (filename.includes('corrupt')) {
throw new Error('File is corrupted');
}
console.log('File processed successfully');
return 'File content';
} catch (error) {
console.error('Error processing file:', error.message);
throw error; // Re-throw if needed
} finally {
// Always close the file, even if an error occurred
if (fileHandle) {
fileHandle.isOpen = false;
console.log(`File ${filename} closed`);
}
}
}
// Usage
try {
const content = processFile('document.txt');
console.log('Content:', content);
} catch (error) {
console.log('Failed to process file');
}
try {
const content = processFile('corrupt-file.txt');
} catch (error) {
console.log('File processing failed as expected');
}
Error Types
Built-in Error Types
// SyntaxError - Invalid syntax
try {
eval('const x = ;'); // Missing value
} catch (error) {
console.log('SyntaxError:', error.message);
}
// ReferenceError - Undefined variable
try {
console.log(undefinedVariable);
} catch (error) {
console.log('ReferenceError:', error.message);
}
// TypeError - Wrong type operation
try {
const obj = null;
obj.someProperty = 'value';
} catch (error) {
console.log('TypeError:', error.message);
}
// RangeError - Value out of range
try {
const arr = new Array(-1);
} catch (error) {
console.log('RangeError:', error.message);
}
// URIError - Invalid URI
try {
decodeURIComponent('%');
} catch (error) {
console.log('URIError:', error.message);
}
// EvalError - Error in eval() function
try {
throw new EvalError('Eval error occurred');
} catch (error) {
console.log('EvalError:', error.message);
}
Custom Error Classes
// Base custom error class
class CustomError extends Error {
constructor(message, code = 'CUSTOM_ERROR') {
super(message);
this.name = this.constructor.name;
this.code = code;
this.timestamp = new Date().toISOString();
// Maintains proper stack trace for where our error was thrown
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
// Specific error types
class ValidationError extends CustomError {
constructor(message, field) {
super(message, 'VALIDATION_ERROR');
this.field = field;
}
}
class NetworkError extends CustomError {
constructor(message, statusCode) {
super(message, 'NETWORK_ERROR');
this.statusCode = statusCode;
}
}
class BusinessLogicError extends CustomError {
constructor(message, operation) {
super(message, 'BUSINESS_LOGIC_ERROR');
this.operation = operation;
}
}
// Usage examples
function validateUser(user) {
if (!user.name) {
throw new ValidationError('Name is required', 'name');
}
if (!user.email) {
throw new ValidationError('Email is required', 'email');
}
if (!user.email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
return true;
}
function processPayment(amount) {
if (amount <= 0) {
throw new BusinessLogicError('Payment amount must be positive', 'processPayment');
}
if (amount > 10000) {
throw new BusinessLogicError('Payment amount exceeds limit', 'processPayment');
}
return { success: true, transactionId: Date.now() };
}
// Error handling with custom errors
try {
const user = { name: '', email: 'invalid-email' };
validateUser(user);
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation failed for field '${error.field}': ${error.message}`);
} else {
console.log('Unexpected error:', error.message);
}
}
Asynchronous Error Handling
Promises and Async/Await
// Error handling with Promises
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!userId) {
reject(new Error('User ID is required'));
return;
}
if (userId === 'invalid') {
reject(new NetworkError('User not found', 404));
return;
}
resolve({ id: userId, name: 'John Doe', email: '[email protected]' });
}, 1000);
});
}
// Using .catch() with Promises
fetchUserData('123')
.then(user => {
console.log('User data:', user);
})
.catch(error => {
console.error('Failed to fetch user:', error.message);
});
// Using async/await with try-catch
async function getUserData(userId) {
try {
const user = await fetchUserData(userId);
console.log('User data:', user);
return user;
} catch (error) {
console.error('Error fetching user:', error.message);
if (error instanceof NetworkError) {
console.log('Network error with status:', error.statusCode);
}
throw error; // Re-throw if needed
}
}
// Multiple async operations with error handling
async function processUserData(userId) {
try {
const user = await fetchUserData(userId);
const preferences = await fetchUserPreferences(userId);
const activity = await fetchUserActivity(userId);
return {
user,
preferences,
activity
};
} catch (error) {
console.error('Error processing user data:', error.message);
// Handle different types of errors
if (error instanceof NetworkError) {
// Retry logic or fallback
console.log('Retrying with fallback data...');
return await getFallbackUserData(userId);
}
throw error;
}
}
// Helper functions
async function fetchUserPreferences(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.7) {
reject(new NetworkError('Failed to fetch preferences', 500));
} else {
resolve({ theme: 'dark', language: 'en' });
}
}, 500);
});
}
async function fetchUserActivity(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ lastLogin: new Date(), loginCount: 42 });
}, 300);
});
}
async function getFallbackUserData(userId) {
return {
user: { id: userId, name: 'Unknown User' },
preferences: { theme: 'light', language: 'en' },
activity: { lastLogin: null, loginCount: 0 }
};
}
Promise.all and Error Handling
// Promise.all with error handling
async function fetchMultipleUsers(userIds) {
try {
const userPromises = userIds.map(id => fetchUserData(id));
const users = await Promise.all(userPromises);
return users;
} catch (error) {
console.error('One or more user fetches failed:', error.message);
throw error;
}
}
// Promise.allSettled for partial success
async function fetchMultipleUsersSafely(userIds) {
const userPromises = userIds.map(id => fetchUserData(id));
const results = await Promise.allSettled(userPromises);
const successful = [];
const failed = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successful.push(result.value);
} else {
failed.push({
userId: userIds[index],
error: result.reason.message
});
}
});
return { successful, failed };
}
// Usage
async function demonstratePromiseErrorHandling() {
const userIds = ['123', '456', 'invalid', '789'];
try {
// This will fail if any promise rejects
const users = await fetchMultipleUsers(userIds);
console.log('All users fetched:', users);
} catch (error) {
console.log('Promise.all failed:', error.message);
}
// This will return both successful and failed results
const results = await fetchMultipleUsersSafely(userIds);
console.log('Successful:', results.successful.length);
console.log('Failed:', results.failed.length);
console.log('Failed details:', results.failed);
}
demonstratePromiseErrorHandling();
Error Boundaries and Recovery
Global Error Handling
// Global error handler for unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
// Log to error tracking service
logErrorToService(event.reason);
// Prevent the default handler
event.preventDefault();
});
// Global error handler for uncaught errors
window.addEventListener('error', (event) => {
console.error('Uncaught error:', event.error);
// Log to error tracking service
logErrorToService(event.error);
// Show user-friendly message
showUserError('An unexpected error occurred. Please try again.');
});
// Error logging service
function logErrorToService(error) {
const errorData = {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
};
// Send to error tracking service (e.g., Sentry, LogRocket)
console.log('Logging error to service:', errorData);
}
// User-friendly error display
function showUserError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-notification';
errorDiv.textContent = message;
errorDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #ff4444;
color: white;
padding: 15px;
border-radius: 5px;
z-index: 10000;
`;
document.body.appendChild(errorDiv);
// Auto-remove after 5 seconds
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
Retry Logic and Circuit Breaker
// Retry mechanism with exponential backoff
async function retryOperation(operation, maxRetries = 3, baseDelay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === maxRetries) {
throw lastError;
}
// Exponential backoff: 1s, 2s, 4s, etc.
const delay = baseDelay * Math.pow(2, attempt - 1);
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Circuit breaker pattern
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureThreshold = 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.failureThreshold) {
this.state = 'OPEN';
}
}
}
// Usage example
const circuitBreaker = new CircuitBreaker(3, 30000);
async function unreliableOperation() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.3) {
reject(new Error('Operation failed'));
} else {
resolve('Operation succeeded');
}
}, 1000);
});
}
async function demonstrateCircuitBreaker() {
for (let i = 0; i < 10; i++) {
try {
const result = await circuitBreaker.execute(unreliableOperation);
console.log(`Attempt ${i + 1}: ${result}`);
} catch (error) {
console.log(`Attempt ${i + 1}: ${error.message}`);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// demonstrateCircuitBreaker();
Debugging Techniques
Console Debugging
// Advanced console methods
function demonstrateConsoleDebugging() {
const user = { name: 'John', age: 30, city: 'New York' };
const users = [
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 },
{ name: 'Bob', age: 35 }
];
// Basic logging
console.log('Basic log:', user);
// Styled logging
console.log('%cStyled message', 'color: red; font-size: 20px;');
// Table display
console.table(users);
// Grouped logging
console.group('User Information');
console.log('Name:', user.name);
console.log('Age:', user.age);
console.groupEnd();
// Conditional logging
console.assert(user.age > 18, 'User must be over 18');
// Trace logging
console.trace('Function call trace');
// Time measurement
console.time('Operation');
// Simulate some work
setTimeout(() => {
console.timeEnd('Operation');
}, 1000);
// Count logging
for (let i = 0; i < 3; i++) {
console.count('Loop iteration');
}
// Clear console
// console.clear();
}
demonstrateConsoleDebugging();
Error Stack Traces
// Custom error with stack trace
function functionA() {
functionB();
}
function functionB() {
functionC();
}
function functionC() {
throw new Error('Error in functionC');
}
// Capture and analyze stack trace
function analyzeStackTrace() {
try {
functionA();
} catch (error) {
console.log('Error message:', error.message);
console.log('Stack trace:', error.stack);
// Parse stack trace
const stackLines = error.stack.split('\n');
console.log('Stack trace lines:', stackLines);
// Extract function names
const functionNames = stackLines
.filter(line => line.includes('at '))
.map(line => {
const match = line.match(/at\s+(\w+)/);
return match ? match[1] : 'anonymous';
});
console.log('Function call chain:', functionNames);
}
}
analyzeStackTrace();
Best Practices
1. Specific Error Handling
// Good: Specific error handling
async function processPayment(paymentData) {
try {
validatePaymentData(paymentData);
const result = await chargeCard(paymentData);
return result;
} catch (error) {
if (error instanceof ValidationError) {
// Handle validation errors
return { success: false, error: 'Invalid payment data' };
} else if (error instanceof NetworkError) {
// Handle network errors
return { success: false, error: 'Payment service unavailable' };
} else {
// Handle unexpected errors
logError(error);
return { success: false, error: 'Payment failed' };
}
}
}
// Avoid: Generic error handling
async function badProcessPayment(paymentData) {
try {
// ... payment logic
} catch (error) {
// Too generic - doesn't help user or developer
console.log('Something went wrong');
}
}
2. Error Logging and Monitoring
// Comprehensive error logging
class ErrorLogger {
constructor() {
this.errors = [];
this.maxErrors = 100;
}
log(error, context = {}) {
const errorEntry = {
id: Date.now(),
message: error.message,
stack: error.stack,
type: error.constructor.name,
timestamp: new Date().toISOString(),
context: context,
userAgent: navigator.userAgent,
url: window.location.href
};
this.errors.push(errorEntry);
// Keep only recent errors
if (this.errors.length > this.maxErrors) {
this.errors.shift();
}
// Send to monitoring service
this.sendToMonitoringService(errorEntry);
}
sendToMonitoringService(errorEntry) {
// In production, send to services like Sentry, LogRocket, etc.
console.log('Sending error to monitoring service:', errorEntry);
}
getErrors() {
return this.errors;
}
clearErrors() {
this.errors = [];
}
}
// Global error logger instance
const errorLogger = new ErrorLogger();
// Usage
try {
riskyOperation();
} catch (error) {
errorLogger.log(error, {
operation: 'riskyOperation',
userId: getCurrentUserId(),
additionalData: { some: 'context' }
});
}
3. User-Friendly Error Messages
// Error message mapping
const ERROR_MESSAGES = {
'VALIDATION_ERROR': 'Please check your input and try again.',
'NETWORK_ERROR': 'Unable to connect. Please check your internet connection.',
'AUTHENTICATION_ERROR': 'Please log in to continue.',
'PERMISSION_ERROR': 'You don\'t have permission to perform this action.',
'RATE_LIMIT_ERROR': 'Too many requests. Please wait a moment and try again.',
'SERVER_ERROR': 'Server error. Please try again later.',
'UNKNOWN_ERROR': 'An unexpected error occurred. Please try again.'
};
function getUserFriendlyMessage(error) {
if (error instanceof ValidationError) {
return ERROR_MESSAGES.VALIDATION_ERROR;
} else if (error instanceof NetworkError) {
return ERROR_MESSAGES.NETWORK_ERROR;
} else if (error.code) {
return ERROR_MESSAGES[error.code] || ERROR_MESSAGES.UNKNOWN_ERROR;
}
return ERROR_MESSAGES.UNKNOWN_ERROR;
}
// Display user-friendly errors
function showErrorToUser(error) {
const message = getUserFriendlyMessage(error);
const errorElement = document.createElement('div');
errorElement.className = 'user-error';
errorElement.textContent = message;
// Add to page
document.body.appendChild(errorElement);
// Auto-remove after 5 seconds
setTimeout(() => {
errorElement.remove();
}, 5000);
}
Summary
Error handling is essential for robust JavaScript applications:
- Try-Catch Blocks: Use for synchronous error handling
- Custom Errors: Create specific error types for better debugging
- Async Error Handling: Handle errors in Promises and async/await
- Global Handlers: Implement for unhandled errors
- Retry Logic: Use for transient failures
- Error Logging: Implement comprehensive error tracking
- User Experience: Provide meaningful error messages
Mastering error handling enables you to build reliable, user-friendly applications that gracefully handle unexpected situations and provide excellent debugging capabilities.
This tutorial is part of the JavaScript Mastery series by syscook.dev