Advanced JavaScript (ES6+)

Error Handling

13 min Lesson 35 of 40

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:

  1. Custom error classes for different error types
  2. Input validation for all operations
  3. Error recovery with default values
  4. 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!