Try/Catch Blocks and Error Objects

Graceful Error Handling in JavaScript

Introduction to Error Handling

Error handling is a critical aspect of writing robust JavaScript applications. Without proper error handling, unexpected issues can crash your program, lead to unintended behavior, or expose security vulnerabilities.

Think of error handling like safety nets for circus performers. The artist may be skilled, but safety nets are essential - not because failure is expected, but because it must be anticipated.

flowchart TD A[Code Execution] --> B{Error Occurs?} B -->|Yes| C[Error Handling] B -->|No| D[Normal Execution] C --> E[Recovery Strategy] E --> A D --> F[Program Continues]

Try/Catch Block Fundamentals

The try/catch statement creates a block of code where exceptions can be "caught" and handled rather than causing the program to crash.

try {
    // Code that might cause an error
    const result = riskyOperation();
} catch (error) {
    // Code that handles the error
    console.error('Something went wrong:', error.message);
}

The basic structure consists of:

The Try Block

The try block wraps code that might throw an exception. JavaScript will monitor this code for exceptions and redirect to the catch block if one occurs.

try {
    // Risky code goes here
    const user = JSON.parse(userDataString); // Might throw if invalid JSON
    document.getElementById('nonexistentElement').textContent = 'Hello'; // Might throw DOM error
    const result = 10 / 0; // Arithmetic operation that might cause issues
}

Real-world example: Imagine you're building an application that fetches user data from localStorage. The data might be corrupted or missing:

try {
    const userData = localStorage.getItem('userData');
    if (!userData) {
        throw new Error('User data not found in localStorage');
    }
    const user = JSON.parse(userData);
    displayUserProfile(user);
} catch (error) {
    console.error('Failed to load user profile:', error.message);
    showLoginForm(); // Fallback behavior
}

The Catch Block

The catch block receives the exception object and contains code to handle the error gracefully.

The error parameter (often named 'error', 'err', or 'e') is an object that contains information about the exception:

try {
    throw new Error('Something bad happened');
} catch (error) {
    console.error(error.name);     // "Error"
    console.error(error.message);  // "Something bad happened"
    console.error(error.stack);    // Stack trace showing where the error occurred
}

You can customize error handling based on the error type:

try {
    // Some risky code
} catch (error) {
    if (error instanceof TypeError) {
        // Handle type errors
    } else if (error instanceof ReferenceError) {
        // Handle reference errors
    } else {
        // Handle all other errors
    }
}

The Finally Block

The finally block contains code that always executes after the try and catch blocks, regardless of whether an exception occurred.

try {
    const connection = openDatabaseConnection();
    // Database operations...
} catch (error) {
    console.error('Database error:', error.message);
} finally {
    // This code always runs, ensuring resources are released
    closeDatabaseConnection();
}

The finally block is perfect for:

Real-world example: Loading indicator in a web application

const showLoader = () => document.getElementById('loader').style.display = 'block';
const hideLoader = () => document.getElementById('loader').style.display = 'none';

async function fetchUserData(userId) {
    showLoader();
    try {
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        const userData = await response.json();
        return userData;
    } catch (error) {
        console.error('Failed to fetch user data:', error);
        displayErrorMessage('Could not load user information. Please try again.');
        return null;
    } finally {
        hideLoader(); // Always hide the loader, whether successful or not
    }
}

Error Objects in JavaScript

JavaScript has several built-in error object types that provide specific information about different error conditions:

classDiagram Error <|-- SyntaxError Error <|-- ReferenceError Error <|-- TypeError Error <|-- RangeError Error <|-- URIError Error <|-- EvalError Error <|-- AggregateError class Error { +String name +String message +String stack }

Creating Custom Error Objects

You can create custom error types by extending the built-in Error class:

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

// Usage
try {
    const email = user.email;
    if (!email.includes('@')) {
        throw new ValidationError('Invalid email format', 'email');
    }
} catch (error) {
    if (error instanceof ValidationError) {
        console.error(`Validation failed for ${error.field}: ${error.message}`);
        highlightField(error.field);
    } else {
        console.error('An unexpected error occurred:', error);
    }
}

Creating custom error classes helps organize your error handling and provides more context about the error's nature.

Real-World Error Handling Strategies

Form Validation

function validateForm(formData) {
    try {
        if (!formData.name) {
            throw new ValidationError('Name is required', 'name');
        }
        
        if (!formData.email || !formData.email.includes('@')) {
            throw new ValidationError('Valid email is required', 'email');
        }
        
        if (formData.password.length < 8) {
            throw new ValidationError('Password must be at least 8 characters', 'password');
        }
        
        return true; // Form is valid
    } catch (error) {
        if (error instanceof ValidationError) {
            displayFieldError(error.field, error.message);
        } else {
            console.error('Validation error:', error);
            displayGeneralError('Form validation failed');
        }
        return false; // Form is invalid
    }
}

API Request Handling

async function fetchData(endpoint) {
    try {
        const response = await fetch(endpoint);
        
        if (!response.ok) {
            if (response.status === 404) {
                throw new NotFoundError(`Resource not found at ${endpoint}`);
            } else if (response.status === 401 || response.status === 403) {
                throw new AuthorizationError('You do not have permission to access this resource');
            } else {
                throw new Error(`HTTP error! Status: ${response.status}`);
            }
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        if (error instanceof NotFoundError) {
            showNotFoundMessage();
        } else if (error instanceof AuthorizationError) {
            redirectToLogin();
        } else if (error.name === 'SyntaxError') {
            console.error('Invalid JSON response from server');
            showServerErrorMessage();
        } else if (error.name === 'TypeError' && error.message.includes('fetch')) {
            showNetworkErrorMessage('Check your internet connection');
        } else {
            console.error('Fetch error:', error);
            showGeneralErrorMessage();
        }
        return null;
    }
}

Best Practices for Error Handling

Be Specific with Try/Catch Blocks

Keep try blocks as small as possible to isolate the exact source of errors:

// Not ideal: Too much in one try block
try {
    const data = JSON.parse(rawData);
    processData(data);
    saveToDatabase(data);
    notifyUser(data);
} catch (error) {
    console.error('Something failed:', error);
}

// Better: Multiple specific try/catch blocks
try {
    const data = JSON.parse(rawData);
} catch (error) {
    console.error('Invalid data format:', error);
    return;
}

try {
    processData(data);
} catch (error) {
    console.error('Processing failed:', error);
    logProcessingError(data, error);
}

// Continue with other operations...

Provide Useful Error Messages

Error messages should be clear, actionable, and helpful for both users and developers:

// Poor error message
throw new Error('Failed');

// Better error message
throw new Error('User authentication failed: Invalid password format');

Don't Catch What You Can't Handle

Only catch errors that you can meaningfully respond to:

try {
    const result = someRiskyOperation();
} catch (error) {
    // If you can't handle it properly, consider re-throwing
    console.error('Operation failed:', error);
    throw error; // Let a higher level catch block handle it
}

Handle Async Errors Properly

Remember that async/await and Promises require different error handling approaches:

// Using try/catch with async/await
async function fetchUserProfile() {
    try {
        const response = await fetch('/api/profile');
        if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
        return await response.json();
    } catch (error) {
        console.error('Profile fetch failed:', error);
        return null;
    }
}

// Using .catch() with Promises
fetch('/api/profile')
    .then(response => {
        if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
        return response.json();
    })
    .then(data => displayProfile(data))
    .catch(error => {
        console.error('Profile fetch failed:', error);
        showErrorMessage();
    });

Common Error Handling Patterns

Fallback Values

function getConfigValue(key) {
    try {
        const config = JSON.parse(localStorage.getItem('appConfig') || '{}');
        return config[key] || DEFAULT_CONFIG[key];
    } catch (error) {
        console.warn(`Could not load config, using defaults: ${error.message}`);
        return DEFAULT_CONFIG[key];
    }
}

Retry Pattern

async function fetchWithRetry(url, maxRetries = 3) {
    let retries = 0;
    while (retries < maxRetries) {
        try {
            const response = await fetch(url);
            if (response.ok) return await response.json();
            throw new Error(`HTTP error! Status: ${response.status}`);
        } catch (error) {
            retries++;
            console.warn(`Attempt ${retries} failed: ${error.message}`);
            if (retries >= maxRetries) throw error;
            // Wait before retry (exponential backoff)
            await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retries - 1)));
        }
    }
}

Error Boundary Pattern (React)

In React applications, Error Boundaries provide a way to catch and handle errors in components:

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false, error: null };
    }
    
    static getDerivedStateFromError(error) {
        return { hasError: true, error };
    }
    
    componentDidCatch(error, errorInfo) {
        console.error('Component error:', error, errorInfo);
        logErrorToService(error, errorInfo);
    }
    
    render() {
        if (this.state.hasError) {
            return <div className="error-ui">
                <h2>Something went wrong</h2>
                <p>{this.state.error.message}</p>
                <button onClick={() => this.setState({ hasError: false })}>Try Again</button>
            </div>;
        }
        
        return this.props.children;
    }
}

Practice Activities

Activity 1: Basic Error Handling

Modify the following function to use try/catch to handle potential errors:

function calculateAverage(numbers) {
    // The input might not be an array or might contain non-numeric values
    // Implement error handling to deal with these cases
    const sum = numbers.reduce((acc, val) => acc + val, 0);
    return sum / numbers.length;
}

Activity 2: Custom Error Class

Create a custom NetworkError class that extends Error and includes additional details like status code and retry information.

Activity 3: Async Error Handling

Implement robust error handling for a function that:

  1. Fetches user data from an API
  2. Processes that data
  3. Saves the result to localStorage

Consider different types of errors that could occur at each step.

Additional Challenge: Error Logging Service

Implement a simple error logging service that:

  1. Captures errors with a universal handler
  2. Formats error details including browser information
  3. Simulates sending errors to a backend service
  4. Provides hooks for different error types

Key Takeaways

Remember: Good error handling is like good insurance - you hope you never need it, but you'll be grateful to have it when something goes wrong!

Further Resources