Error Handling
Welcome to error handling in JavaScript! In this lesson, we'll explore how to handle errors gracefully, create custom errors, and implement robust error handling strategies. Proper error handling is crucial for building reliable applications.
Understanding Errors
JavaScript errors are objects that contain information about what went wrong in your code:
Key Concept: Errors stop code execution unless they're caught and handled. Good error handling prevents crashes, provides useful feedback, and helps with debugging.
Try...Catch...Finally
The try...catch...finally statement allows you to handle errors gracefully:
Basic Try...Catch:
try {
// Code that might throw an error
const result = riskyOperation();
console.log(result);
} catch (error) {
// Handle the error
console.error('An error occurred:', error.message);
}
console.log('Program continues...');
With Finally Block:
try {
const file = openFile('data.txt');
processFile(file);
} catch (error) {
console.error('Error processing file:', error);
} finally {
// Always executes, regardless of error
closeFile(file);
console.log('Cleanup complete');
}
Best Practice: Use finally blocks for cleanup operations that must run regardless of whether an error occurred - closing files, releasing resources, or resetting states.
Error Types
JavaScript has several built-in error types for different situations:
Built-in Error Types:
1. Error: Generic error
const error = new Error('Something went wrong');
2. SyntaxError: Invalid syntax
eval('invalid code{'); // SyntaxError
3. ReferenceError: Invalid reference
console.log(nonExistentVariable); // ReferenceError
4. TypeError: Wrong type
null.toString(); // TypeError
5. RangeError: Value out of range
new Array(-1); // RangeError
6. URIError: Invalid URI handling
decodeURIComponent('%'); // URIError
Throwing Errors
Use the throw keyword to create and throw errors:
Throwing Built-in Errors:
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero is not allowed');
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error(error.message); // Division by zero is not allowed
}
Throwing Type-Specific Errors:
function setAge(age) {
if (typeof age !== 'number') {
throw new TypeError('Age must be a number');
}
if (age < 0 || age > 150) {
throw new RangeError('Age must be between 0 and 150');
}
return age;
}
try {
setAge('twenty'); // TypeError
} catch (error) {
console.error(`${error.name}: ${error.message}`);
}
Custom Error Classes
Create custom error classes for specific error scenarios in your application:
Creating Custom Errors:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = 'DatabaseError';
this.query = query;
this.timestamp = new Date();
}
}
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = 'AuthenticationError';
this.statusCode = 401;
}
}
// Usage
function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regex.test(email)) {
throw new ValidationError(`Invalid email format: ${email}`);
}
return true;
}
try {
validateEmail('invalid-email');
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation failed:', error.message);
} else {
console.error('Unexpected error:', error);
}
}
Advantage: Custom errors allow you to handle different error types differently and include additional context specific to your application.
Error Handling in Async/Await
Handling errors in asynchronous code requires special attention:
Async/Await Error Handling:
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
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);
throw error; // Re-throw if caller needs to handle it
}
}
// Using the async function
async function displayUser(id) {
try {
const user = await fetchUser(id);
console.log('User:', user);
} catch (error) {
console.error('Error displaying user:', error);
}
}
Promise Error Handling:
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Fetch failed:', error);
})
.finally(() => {
console.log('Request completed');
});
Error Boundaries Pattern
Implement error boundaries to catch and handle errors in specific parts of your application:
Error Boundary Pattern:
class ErrorBoundary {
constructor(fallback) {
this.fallback = fallback;
}
wrap(fn) {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
console.error('Error caught by boundary:', error);
return this.fallback(error);
}
};
}
}
// Usage
const boundary = new ErrorBoundary((error) => {
return { error: true, message: error.message };
});
const safeFunction = boundary.wrap(async (userId) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('User not found');
return response.json();
});
// This won't crash the application
const result = await safeFunction(999);
console.log(result); // { error: true, message: 'User not found' }
Error Stack Traces
Error objects include stack traces that help with debugging:
Using Stack Traces:
function levelThree() {
throw new Error('Something went wrong');
}
function levelTwo() {
levelThree();
}
function levelOne() {
levelTwo();
}
try {
levelOne();
} catch (error) {
console.log(error.name); // Error
console.log(error.message); // Something went wrong
console.log(error.stack); // Full stack trace
// Error: Something went wrong
// at levelThree (...)
// at levelTwo (...)
// at levelOne (...)
}
Global Error Handling
Set up global error handlers for unhandled errors and promise rejections:
Global Error Handlers:
// Handle uncaught errors
window.addEventListener('error', (event) => {
console.error('Global error caught:', event.error);
// Log to error tracking service
logErrorToService(event.error);
// Prevent default browser error handling
event.preventDefault();
});
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
// Log to error tracking service
logErrorToService(event.reason);
// Prevent default browser handling
event.preventDefault();
});
// Node.js global error handlers
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
// Clean up and exit
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
});
Important: Global error handlers should be used as a last resort. Always try to handle errors at the appropriate level in your code where you have the necessary context.
Error Recovery Strategies
Implement strategies to recover from errors gracefully:
Retry Pattern:
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i <= maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (i === maxRetries) {
throw new Error(`Failed after ${maxRetries} retries: ${error.message}`);
}
// Wait before retrying (exponential backoff)
const delay = Math.pow(2, i) * 1000;
console.log(`Retry ${i + 1} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Fallback Pattern:
async function getDataWithFallback() {
try {
return await fetchFromPrimaryAPI();
} catch (primaryError) {
console.warn('Primary API failed, trying backup');
try {
return await fetchFromBackupAPI();
} catch (backupError) {
console.error('Both APIs failed, using cache');
return getCachedData();
}
}
}
Defensive Programming
Write defensive code to prevent errors before they occur:
Input Validation:
function processUser(user) {
// Validate input
if (!user) {
throw new TypeError('User is required');
}
if (typeof user !== 'object') {
throw new TypeError('User must be an object');
}
if (!user.name || typeof user.name !== 'string') {
throw new ValidationError('User must have a valid name');
}
if (!user.email || typeof user.email !== 'string') {
throw new ValidationError('User must have a valid email');
}
// Process user safely
return {
name: user.name.trim(),
email: user.email.toLowerCase().trim()
};
}
Optional Chaining and Nullish Coalescing:
function getUserCity(user) {
// Safe property access
const city = user?.address?.city ?? 'Unknown';
return city;
}
// Without optional chaining, this would throw if user or address is null
const city = getUserCity(null); // 'Unknown' instead of error
Error Logging and Monitoring
Implement comprehensive error logging for production applications:
Error Logger Class:
class ErrorLogger {
constructor(environment = 'development') {
this.environment = environment;
this.logs = [];
}
log(error, context = {}) {
const errorLog = {
timestamp: new Date().toISOString(),
environment: this.environment,
error: {
name: error.name,
message: error.message,
stack: error.stack
},
context,
userAgent: navigator.userAgent,
url: window.location.href
};
this.logs.push(errorLog);
// Log to console in development
if (this.environment === 'development') {
console.error('Error logged:', errorLog);
}
// Send to error tracking service in production
if (this.environment === 'production') {
this.sendToService(errorLog);
}
return errorLog;
}
sendToService(errorLog) {
// Send to error tracking service (Sentry, LogRocket, etc.)
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorLog)
}).catch(err => {
console.error('Failed to send error log:', err);
});
}
getLogs() {
return this.logs;
}
clearLogs() {
this.logs = [];
}
}
// Usage
const logger = new ErrorLogger('production');
try {
riskyOperation();
} catch (error) {
logger.log(error, {
userId: currentUser.id,
action: 'data-processing'
});
}
Real-World Example: API Client with Error Handling
Building a robust API client with comprehensive error handling:
API Client:
class APIError extends Error {
constructor(message, statusCode, response) {
super(message);
this.name = 'APIError';
this.statusCode = statusCode;
this.response = response;
}
}
class APIClient {
constructor(baseURL, options = {}) {
this.baseURL = baseURL;
this.timeout = options.timeout || 10000;
this.retries = options.retries || 3;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const controller = new AbortController();
// Timeout handling
const timeoutId = setTimeout(() => {
controller.abort();
}, this.timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
// Handle HTTP errors
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new APIError(
errorData.message || `HTTP ${response.status}`,
response.status,
errorData
);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
// Handle different error types
if (error.name === 'AbortError') {
throw new APIError('Request timeout', 408, null);
}
if (error instanceof APIError) {
throw error;
}
// Network errors
throw new APIError(
`Network error: ${error.message}`,
0,
null
);
}
}
async get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
}
// Usage
const api = new APIClient('https://api.example.com', {
timeout: 5000,
retries: 3
});
async function loadUserData(userId) {
try {
const user = await api.get(`/users/${userId}`);
console.log('User loaded:', user);
return user;
} catch (error) {
if (error instanceof APIError) {
switch (error.statusCode) {
case 404:
console.error('User not found');
break;
case 401:
console.error('Unauthorized');
// Redirect to login
break;
case 408:
console.error('Request timeout');
// Retry or show message
break;
default:
console.error('API error:', error.message);
}
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
Practice Exercise:
Task: Create a safe calculator with comprehensive error handling:
- Custom error classes for different error types
- Input validation for all operations
- Error recovery with default values
- Error logging
Solution:
class CalculatorError extends Error {
constructor(message) {
super(message);
this.name = 'CalculatorError';
}
}
class DivisionByZeroError extends CalculatorError {
constructor() {
super('Division by zero is not allowed');
this.name = 'DivisionByZeroError';
}
}
class InvalidInputError extends CalculatorError {
constructor(input) {
super(`Invalid input: ${input}. Expected a number.`);
this.name = 'InvalidInputError';
this.input = input;
}
}
class SafeCalculator {
constructor() {
this.errors = [];
}
validateInput(a, b) {
if (typeof a !== 'number' || isNaN(a)) {
throw new InvalidInputError(a);
}
if (typeof b !== 'number' || isNaN(b)) {
throw new InvalidInputError(b);
}
}
logError(error) {
this.errors.push({
timestamp: new Date(),
error: error.message,
type: error.name
});
}
add(a, b) {
try {
this.validateInput(a, b);
return a + b;
} catch (error) {
this.logError(error);
throw error;
}
}
subtract(a, b) {
try {
this.validateInput(a, b);
return a - b;
} catch (error) {
this.logError(error);
throw error;
}
}
multiply(a, b) {
try {
this.validateInput(a, b);
return a * b;
} catch (error) {
this.logError(error);
throw error;
}
}
divide(a, b) {
try {
this.validateInput(a, b);
if (b === 0) {
throw new DivisionByZeroError();
}
return a / b;
} catch (error) {
this.logError(error);
throw error;
}
}
safeOperation(operation, a, b, defaultValue = null) {
try {
return this[operation](a, b);
} catch (error) {
console.warn(`Operation failed, returning default: ${defaultValue}`);
return defaultValue;
}
}
getErrors() {
return this.errors;
}
}
// Usage
const calc = new SafeCalculator();
console.log(calc.add(5, 3)); // 8
console.log(calc.safeOperation('divide', 10, 0, 0)); // 0 (with warning)
try {
calc.divide(10, 0);
} catch (error) {
console.error(`${error.name}: ${error.message}`);
}
console.log(calc.getErrors()); // Array of logged errors
Summary
In this lesson, you learned:
- Use try...catch...finally to handle errors gracefully
- JavaScript has multiple built-in error types for different situations
- Create custom error classes for application-specific errors
- Handle async errors with try...catch in async/await and .catch() in promises
- Implement error boundaries to contain errors in specific code sections
- Set up global error handlers as a safety net
- Use retry and fallback patterns for error recovery
- Implement comprehensive error logging for production applications
Congratulations! You've completed Module 6: Modules and Code Organization. You now have a solid understanding of ES6 modules, bundlers, design patterns, and error handling - essential skills for professional JavaScript development!