Error Handling in JavaScript

Try/Catch Blocks and Error Management

Why Error Handling Matters

Imagine you're driving a car, and suddenly the engine starts making strange noises. Would you prefer to ignore it and keep driving, or would you rather have a dashboard warning light that tells you exactly what's wrong? Error handling in JavaScript is like that dashboard - it helps us identify, understand, and gracefully handle problems in our code before they crash our entire application.

Understanding Errors in JavaScript

JavaScript has several built-in error types, each serving a specific purpose:

graph TD A[Error] --> B[SyntaxError] A --> C[ReferenceError] A --> D[TypeError] A --> E[RangeError] A --> F[URIError] A --> G[EvalError] A --> H[Custom Errors]

The Try/Catch Statement

The try/catch statement allows you to "try" a block of code and "catch" any errors that occur:

// Basic try/catch syntax
try {
    // Code that might throw an error
    riskyOperation();
} catch (error) {
    // Handle the error
    console.error('An error occurred:', error.message);
}

How It Works

flowchart TD A[Enter try block] --> B{Error occurs?} B -->|No| C[Execute all code in try] B -->|Yes| D[Jump to catch block] C --> E[Skip catch block] D --> F[Execute catch block] E --> G[Continue execution] F --> G

Basic Error Handling Examples

Example 1: Handling Reference Errors

function greetUser(name) {
    try {
        // Attempting to use an undefined variable
        console.log(message); // 'message' is not defined
        return `Hello, ${name}!`;
    } catch (error) {
        console.error('Caught an error:', error.name);
        console.error('Error message:', error.message);
        // Provide a fallback behavior
        return `Hello, ${name}! (Error occurred)`;
    }
}

console.log(greetUser('Alice'));
// Output:
// Caught an error: ReferenceError
// Error message: message is not defined
// Hello, Alice! (Error occurred)

Example 2: Handling Type Errors

function calculateTotal(items) {
    try {
        // This will fail if items is not an array
        return items.reduce((sum, item) => sum + item.price, 0);
    } catch (error) {
        console.error('Error calculating total:', error.message);
        // Return a safe default value
        return 0;
    }
}

// Works fine with valid data
console.log(calculateTotal([{price: 10}, {price: 20}])); // 30

// Handles error gracefully with invalid data
console.log(calculateTotal(null)); // 0
console.log(calculateTotal("not an array")); // 0

The Finally Block

The finally block runs regardless of whether an error occurred:

function processFile(filename) {
    let file;
    
    try {
        file = openFile(filename);
        const data = readFile(file);
        processData(data);
        return 'Success';
    } catch (error) {
        console.error('Error processing file:', error.message);
        return 'Failed';
    } finally {
        // This always runs, whether there was an error or not
        if (file) {
            closeFile(file);
            console.log('File closed');
        }
    }
}

// Simulated file operations
function openFile(name) { 
    console.log(`Opening ${name}`); 
    return { name, isOpen: true }; 
}

function readFile(file) { 
    if (Math.random() > 0.5) {
        throw new Error('Read error');
    }
    return 'file contents'; 
}

function processData(data) { 
    console.log('Processing:', data); 
}

function closeFile(file) { 
    file.isOpen = false; 
}

// Test the function
console.log(processFile('data.txt'));
console.log(processFile('data.txt'));

Throwing Custom Errors

You can create and throw your own errors to handle specific situations:

// Creating custom error classes
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'ValidationError';
    }
}

class AuthenticationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'AuthenticationError';
    }
}

// Using custom errors
function validateUser(user) {
    if (!user) {
        throw new ValidationError('User object is required');
    }
    
    if (!user.email) {
        throw new ValidationError('Email is required');
    }
    
    if (!user.email.includes('@')) {
        throw new ValidationError('Invalid email format');
    }
    
    return true;
}

function authenticateUser(credentials) {
    if (!credentials.token) {
        throw new AuthenticationError('Authentication token missing');
    }
    
    if (credentials.token !== 'valid-token') {
        throw new AuthenticationError('Invalid authentication token');
    }
    
    return true;
}

// Using the functions with proper error handling
try {
    validateUser({ email: 'invalid-email' });
} catch (error) {
    if (error instanceof ValidationError) {
        console.log('Validation failed:', error.message);
    } else {
        console.log('Unexpected error:', error);
    }
}

try {
    authenticateUser({ token: 'invalid-token' });
} catch (error) {
    if (error instanceof AuthenticationError) {
        console.log('Authentication failed:', error.message);
    } else {
        console.log('Unexpected error:', error);
    }
}

Error Handling in Asynchronous Code

Try/Catch with Async/Await

async function fetchUserData(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();
        
        if (!userData.email) {
            throw new ValidationError('User data missing email');
        }
        
        return userData;
    } catch (error) {
        // Different handling based on error type
        if (error instanceof ValidationError) {
            console.error('Data validation error:', error.message);
            return null;
        } else if (error.name === 'TypeError') {
            console.error('Network error:', error.message);
            return null;
        } else {
            console.error('Unexpected error:', error);
            throw error; // Re-throw unexpected errors
        }
    }
}

// Usage
async function displayUserProfile(userId) {
    const userData = await fetchUserData(userId);
    
    if (userData) {
        console.log('User profile:', userData);
    } else {
        console.log('Failed to load user profile');
    }
}

Error Handling with Promises

function fetchDataWithPromises(url) {
    return fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            return response.json();
        })
        .then(data => {
            if (!data.isValid) {
                throw new ValidationError('Invalid data received');
            }
            return data;
        })
        .catch(error => {
            // Handle different types of errors
            if (error instanceof ValidationError) {
                console.error('Validation error:', error.message);
                return { error: 'Invalid data', details: error.message };
            } else if (error.name === 'TypeError') {
                console.error('Network error:', error.message);
                return { error: 'Network issue', details: error.message };
            } else {
                console.error('Unexpected error:', error);
                return { error: 'Unknown error', details: error.message };
            }
        });
}

Best Practices for Error Handling

1. Be Specific with Error Types

// Bad practice - generic error handling
try {
    doSomething();
} catch (e) {
    console.log('Error occurred');
}

// Good practice - specific error handling
try {
    doSomething();
} catch (error) {
    if (error instanceof TypeError) {
        console.error('Type error:', error.message);
        // Handle type error specifically
    } else if (error instanceof RangeError) {
        console.error('Range error:', error.message);
        // Handle range error specifically
    } else {
        console.error('Unexpected error:', error);
        // Handle or re-throw unexpected errors
    }
}

2. Don't Swallow Errors

// Bad practice - silently swallowing errors
try {
    riskyOperation();
} catch (error) {
    // Error is caught but not handled or logged
}

// Good practice - always handle or log errors
try {
    riskyOperation();
} catch (error) {
    console.error('Operation failed:', error);
    // Either handle the error or re-throw it
    throw error;
}

3. Provide Meaningful Error Messages

// Bad practice - vague error message
function divide(a, b) {
    if (b === 0) {
        throw new Error('Error');
    }
    return a / b;
}

// Good practice - descriptive error message
function divide(a, b) {
    if (b === 0) {
        throw new Error(`Cannot divide ${a} by zero`);
    }
    return a / b;
}

Real-World Error Handling Patterns

Pattern 1: Error Boundary for UI Components

class ErrorBoundary {
    constructor() {
        this.hasError = false;
        this.error = null;
    }
    
    static getDerivedStateFromError(error) {
        return { hasError: true, error };
    }
    
    componentDidCatch(error, errorInfo) {
        console.error('Component error:', error);
        console.error('Error info:', errorInfo);
        
        // Log error to error reporting service
        logErrorToService(error, errorInfo);
    }
    
    render() {
        if (this.hasError) {
            return `

Something went wrong

${this.error.message}

`; } return this.children; } }

Pattern 2: Global Error Handler

// Global error handler for uncaught errors
window.onerror = function(message, source, lineno, colno, error) {
    console.error('Global error caught:', {
        message,
        source,
        line: lineno,
        column: colno,
        error
    });
    
    // Log to error tracking service
    logErrorToService({
        type: 'uncaught',
        message,
        source,
        line: lineno,
        column: colno,
        stack: error?.stack
    });
    
    // Prevent default browser error handling
    return true;
};

// Global handler for unhandled promise rejections
window.onunhandledrejection = function(event) {
    console.error('Unhandled promise rejection:', event.reason);
    
    // Log to error tracking service
    logErrorToService({
        type: 'unhandledRejection',
        reason: event.reason,
        promise: event.promise
    });
    
    // Prevent default browser handling
    event.preventDefault();
};

Error Logging and Monitoring

// Simple error logging service
class ErrorLogger {
    static log(error, context = {}) {
        const errorInfo = {
            timestamp: new Date().toISOString(),
            message: error.message,
            stack: error.stack,
            type: error.name,
            context,
            url: window.location.href,
            userAgent: navigator.userAgent
        };
        
        // In development, log to console
        if (process.env.NODE_ENV === 'development') {
            console.error('Error logged:', errorInfo);
        }
        
        // In production, send to error tracking service
        if (process.env.NODE_ENV === 'production') {
            fetch('/api/log-error', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(errorInfo)
            }).catch(err => {
                console.error('Failed to log error:', err);
            });
        }
    }
}

// Usage example
try {
    performRiskyOperation();
} catch (error) {
    ErrorLogger.log(error, {
        operation: 'performRiskyOperation',
        userId: currentUser.id,
        additionalInfo: 'Failed during data processing'
    });
    
    // Handle the error for the user
    showErrorMessage('Operation failed. Please try again.');
}

Practice Exercises

Exercise 1: Safe JSON Parser

Create a function that safely parses JSON and handles errors:

function safeJSONParse(jsonString, defaultValue = null) {
    // Your code here
    // Should parse JSON string and return parsed object
    // If parsing fails, return defaultValue
    // Log appropriate error messages
}

// Test cases
console.log(safeJSONParse('{"name": "John"}')); // Should return object
console.log(safeJSONParse('invalid json')); // Should return null
console.log(safeJSONParse('invalid json', {})); // Should return {}

Exercise 2: Retry with Error Handling

Create a function that retries an operation with error handling:

async function retryOperation(operation, maxAttempts = 3) {
    // Your code here
    // Should attempt operation up to maxAttempts times
    // Should handle and log errors
    // Should wait between retries
    // Should throw error if all attempts fail
}

// Test with a flaky operation
async function flakyOperation() {
    if (Math.random() > 0.7) {
        return 'Success!';
    }
    throw new Error('Random failure');
}

Key Takeaways

Additional Resources