JavaScript Essentials

Error Handling: try, catch, and finally

45 min Lesson 30 of 60

Understanding Runtime Errors

No matter how carefully you write your code, errors will happen. Users will enter unexpected input, network requests will fail, APIs will return malformed data, and files will go missing. The difference between a fragile application and a robust one is how it handles these inevitable errors. JavaScript provides a powerful error handling mechanism through try, catch, and finally blocks that allow you to gracefully recover from errors instead of letting your entire application crash.

There are three main categories of errors in JavaScript. Syntax errors occur when the code violates the language grammar and are caught before the code even runs. Runtime errors occur during execution when an operation fails -- such as accessing a property on undefined or calling a function that does not exist. Logical errors occur when the code runs without throwing an error but produces incorrect results. This lesson focuses on runtime errors and how to handle them effectively using structured error handling.

Example: Types of Errors

// Syntax Error -- caught before execution (cannot be caught with try/catch)
// const x = ; // SyntaxError: Unexpected token

// Runtime Error -- occurs during execution (CAN be caught with try/catch)
const obj = undefined;
// obj.name; // TypeError: Cannot read properties of undefined

// Logical Error -- no error thrown, but wrong result
function calculateArea(width, height) {
    return width + height; // Bug: should be width * height
}
console.log(calculateArea(5, 3)); // 8 (wrong, should be 15)

The try...catch Statement

The try...catch statement is the foundation of error handling in JavaScript. You wrap code that might throw an error inside a try block, and if an error occurs, execution jumps immediately to the catch block instead of crashing the program.

Example: Basic try...catch

try {
    // Code that might throw an error
    const data = JSON.parse('{"name": "Alice"}');
    console.log(data.name); // "Alice"
} catch (error) {
    // This block runs ONLY if an error occurs in the try block
    console.log('An error occurred:', error.message);
}

console.log('Program continues running!');

// When an error actually occurs:
try {
    const data = JSON.parse('invalid json string');
    console.log(data); // This line never executes
} catch (error) {
    console.log('Caught an error!');
    console.log('Message:', error.message);
    // "Unexpected token i in JSON at position 0"
}

console.log('Still running after the error!');
Note: The try...catch statement only catches runtime errors in synchronous code within the try block. It does not catch syntax errors (which prevent the code from running at all) or errors in asynchronous callbacks like setTimeout. For asynchronous error handling with Promises and async/await, you will learn additional patterns in later lessons.

The Error Object

When JavaScript throws an error, it creates an Error object with three important properties that help you understand what went wrong and where.

Example: Error Object Properties

try {
    const result = undeclaredVariable + 10;
} catch (error) {
    // The error object has three key properties:

    // 1. name -- The type of error
    console.log('Name:', error.name);
    // "ReferenceError"

    // 2. message -- A human-readable description
    console.log('Message:', error.message);
    // "undeclaredVariable is not defined"

    // 3. stack -- The call stack trace (for debugging)
    console.log('Stack:', error.stack);
    // "ReferenceError: undeclaredVariable is not defined
    //     at <anonymous>:2:20"
}

// You can also check the error type
try {
    null.toString();
} catch (error) {
    console.log(error instanceof TypeError); // true
    console.log(error instanceof ReferenceError); // false
}

The catch Parameter

The catch block receives the error object as a parameter. You can name this parameter anything, though error, err, and e are the most common conventions. In modern JavaScript (ES2019+), you can also omit the parameter entirely if you do not need the error information.

Example: Catch Parameter Variations

// Standard: named error parameter
try {
    JSON.parse('bad json');
} catch (error) {
    console.log(error.message);
}

// Common short form
try {
    JSON.parse('bad json');
} catch (err) {
    console.log(err.message);
}

// Single letter (common in compact code)
try {
    JSON.parse('bad json');
} catch (e) {
    console.log(e.message);
}

// Optional catch binding (ES2019+) -- no parameter needed
try {
    JSON.parse('bad json');
} catch {
    console.log('JSON parsing failed, using default values.');
}

The finally Block

The finally block runs no matter what -- whether the try block completes successfully or an error is caught. This makes it perfect for cleanup operations like closing connections, releasing resources, or hiding loading indicators.

Example: try...catch...finally

function processData(jsonString) {
    console.log('Starting data processing...');

    try {
        const data = JSON.parse(jsonString);
        console.log('Data parsed successfully:', data);
        return data;
    } catch (error) {
        console.log('Error parsing data:', error.message);
        return null;
    } finally {
        // This ALWAYS runs, even after a return statement
        console.log('Processing complete. Cleaning up...');
    }
}

// Successful case
processData('{"status": "ok"}');
// "Starting data processing..."
// "Data parsed successfully: {status: 'ok'}"
// "Processing complete. Cleaning up..."

// Error case
processData('not json');
// "Starting data processing..."
// "Error parsing data: Unexpected token ..."
// "Processing complete. Cleaning up..."

Example: Real-World finally Usage

// Simulating a database connection
function queryDatabase(query) {
    let connection = null;

    try {
        connection = openConnection(); // Might throw
        const result = connection.execute(query); // Might throw
        return result;
    } catch (error) {
        console.error('Database query failed:', error.message);
        return null;
    } finally {
        // Always close the connection, even if an error occurred
        if (connection) {
            connection.close();
            console.log('Connection closed.');
        }
    }
}

// Loading indicator pattern
function fetchUserData(userId) {
    showLoadingSpinner(); // Show spinner before starting

    try {
        const user = getUserFromAPI(userId);
        displayUserProfile(user);
    } catch (error) {
        displayErrorMessage('Could not load user profile.');
    } finally {
        hideLoadingSpinner(); // Always hide spinner when done
    }
}
Pro Tip: The finally block executes even if a return statement is reached inside the try or catch blocks. However, if you put a return in the finally block itself, it will override the return value from try or catch. Avoid returning values from finally to prevent confusion.

Throwing Custom Errors

You can throw your own errors using the throw statement. This is essential for enforcing business rules, validating input, and creating meaningful error messages that help callers understand what went wrong.

Example: Throwing Errors

// throw new Error(message) creates and throws an Error object
function divide(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
        throw new Error('Both arguments must be numbers.');
    }
    if (b === 0) {
        throw new Error('Cannot divide by zero.');
    }
    return a / b;
}

try {
    console.log(divide(10, 2));  // 5
    console.log(divide(10, 0));  // Throws!
} catch (error) {
    console.log('Error:', error.message);
    // "Cannot divide by zero."
}

// Validating function arguments
function createUser(name, email) {
    if (!name || typeof name !== 'string') {
        throw new Error('Name is required and must be a string.');
    }
    if (!email || !email.includes('@')) {
        throw new Error('A valid email address is required.');
    }
    return { name, email, createdAt: new Date() };
}

try {
    const user = createUser('', 'invalid');
} catch (error) {
    console.log(error.message);
    // "Name is required and must be a string."
}

try {
    const user = createUser('Alice', 'not-an-email');
} catch (error) {
    console.log(error.message);
    // "A valid email address is required."
}
Warning: You can technically throw any value in JavaScript -- strings, numbers, or objects -- not just Error objects. However, you should always throw Error objects because they include the name, message, and stack properties that are essential for debugging. Throwing a plain string like throw 'something failed' loses the stack trace and makes bugs much harder to track down.

Built-in Error Types

JavaScript provides several built-in error types, each representing a specific category of problem. Understanding these types helps you write more targeted error handling and throw more descriptive errors in your own code.

Example: JavaScript Error Types

// TypeError -- wrong type used in an operation
try {
    null.toString();
} catch (e) {
    console.log(e.name); // "TypeError"
    console.log(e.message); // "Cannot read properties of null"
}

try {
    const num = 42;
    num(); // Trying to call a number as a function
} catch (e) {
    console.log(e.name); // "TypeError"
}

// ReferenceError -- accessing an undeclared variable
try {
    console.log(nonExistentVariable);
} catch (e) {
    console.log(e.name); // "ReferenceError"
    console.log(e.message); // "nonExistentVariable is not defined"
}

// RangeError -- value is outside the allowed range
try {
    const arr = new Array(-1); // Negative array length
} catch (e) {
    console.log(e.name); // "RangeError"
    console.log(e.message); // "Invalid array length"
}

try {
    const num = 1;
    num.toFixed(200); // Maximum is 100
} catch (e) {
    console.log(e.name); // "RangeError"
}

// SyntaxError -- thrown by JSON.parse and eval with invalid syntax
try {
    JSON.parse('{invalid}');
} catch (e) {
    console.log(e.name); // "SyntaxError"
}

// URIError -- invalid URI encoding/decoding
try {
    decodeURIComponent('%');
} catch (e) {
    console.log(e.name); // "URIError"
    console.log(e.message); // "URI malformed"
}

Handling Specific Error Types

You can use instanceof to check the error type and handle different errors differently within a single catch block.

Example: Type-Specific Error Handling

function processInput(input) {
    try {
        const data = JSON.parse(input);
        const result = data.items.map(item => item.name.toUpperCase());
        return result;
    } catch (error) {
        if (error instanceof SyntaxError) {
            console.log('Invalid JSON format. Please check your input.');
        } else if (error instanceof TypeError) {
            console.log('Data structure is invalid. Expected items array with name properties.');
        } else if (error instanceof RangeError) {
            console.log('A value is out of the expected range.');
        } else {
            console.log('An unexpected error occurred:', error.message);
        }
        return [];
    }
}

// SyntaxError path
processInput('not json');
// "Invalid JSON format. Please check your input."

// TypeError path (missing items property)
processInput('{"data": "hello"}');
// "Data structure is invalid. Expected items array with name properties."

// Success path
processInput('{"items": [{"name": "apple"}, {"name": "banana"}]}');
// Returns: ["APPLE", "BANANA"]

Creating Custom Error Classes

For larger applications, built-in error types are often not specific enough. You can create custom error classes that extend the Error class to represent specific failure conditions in your application.

Example: Custom Error Classes

// Base custom error
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.name = 'AppError';
        this.statusCode = statusCode;
    }
}

// Specific error types
class ValidationError extends AppError {
    constructor(message, field) {
        super(message, 400);
        this.name = 'ValidationError';
        this.field = field;
    }
}

class NotFoundError extends AppError {
    constructor(resource, id) {
        super(`${resource} with ID ${id} was not found.`, 404);
        this.name = 'NotFoundError';
        this.resource = resource;
        this.resourceId = id;
    }
}

class AuthenticationError extends AppError {
    constructor(message = 'Authentication required.') {
        super(message, 401);
        this.name = 'AuthenticationError';
    }
}

// Using custom errors
function getUser(id) {
    if (typeof id !== 'number' || id < 1) {
        throw new ValidationError('User ID must be a positive number.', 'id');
    }

    const users = { 1: 'Alice', 2: 'Bob' };
    if (!users[id]) {
        throw new NotFoundError('User', id);
    }

    return { id, name: users[id] };
}

try {
    const user = getUser(5);
} catch (error) {
    if (error instanceof NotFoundError) {
        console.log(`404: ${error.message}`);
        console.log(`Resource: ${error.resource}, ID: ${error.resourceId}`);
    } else if (error instanceof ValidationError) {
        console.log(`Validation failed on field: ${error.field}`);
        console.log(error.message);
    } else if (error instanceof AuthenticationError) {
        console.log('Please log in to continue.');
    } else {
        console.log('Unexpected error:', error.message);
    }
}
// "404: User with ID 5 was not found."
// "Resource: User, ID: 5"

Error Handling Patterns

Effective error handling follows established patterns that make your code more maintainable and predictable. Here are the most important patterns to know.

Nested try...catch Blocks

You can nest try...catch blocks when different sections of code need different error handling strategies.

Example: Nested try...catch

function processUserData(jsonString) {
    let rawData;

    // Outer try: handle JSON parsing errors
    try {
        rawData = JSON.parse(jsonString);
    } catch (error) {
        console.log('Failed to parse JSON input.');
        return null;
    }

    // Inner try: handle data processing errors
    try {
        const user = {
            name: rawData.name.trim(),
            email: rawData.email.toLowerCase(),
            age: parseInt(rawData.age, 10)
        };

        if (isNaN(user.age)) {
            throw new Error('Age must be a valid number.');
        }

        return user;
    } catch (error) {
        console.log('Failed to process user data:', error.message);
        return null;
    }
}

// Test different failure scenarios
processUserData('not json');
// "Failed to parse JSON input."

processUserData('{"name": "Alice", "email": "alice@test.com", "age": "abc"}');
// "Failed to process user data: Age must be a valid number."

processUserData('{"name": "Alice", "email": "alice@test.com", "age": "25"}');
// Returns: {name: "Alice", email: "alice@test.com", age: 25}

Rethrowing Errors

Sometimes you want to catch an error, inspect it or log it, and then rethrow it so that a higher-level handler can deal with it. This is called rethrowing.

Example: Rethrowing Errors

function parseConfig(configString) {
    try {
        const config = JSON.parse(configString);

        // We only want to handle validation errors here
        if (!config.apiKey) {
            throw new ValidationError('API key is required.', 'apiKey');
        }
        if (!config.baseUrl) {
            throw new ValidationError('Base URL is required.', 'baseUrl');
        }

        return config;
    } catch (error) {
        // Only handle ValidationErrors at this level
        if (error instanceof ValidationError) {
            console.log(`Config validation failed: ${error.message}`);
            throw error; // Rethrow for the caller to handle
        }

        // For all other errors (like SyntaxError), wrap and rethrow
        throw new Error(`Failed to parse config: ${error.message}`);
    }
}

// The caller handles the rethrown error
try {
    const config = parseConfig('{"apiKey": ""}');
} catch (error) {
    if (error instanceof ValidationError) {
        console.log('Please fix your configuration file.');
    } else {
        console.log('Configuration error:', error.message);
    }
}

Graceful Degradation

Graceful degradation means your application continues to work -- even if in a reduced capacity -- when errors occur. Instead of crashing, you provide fallback behavior, default values, or user-friendly error messages.

Example: Graceful Degradation Patterns

// Pattern 1: Default values on failure
function loadUserPreferences(userId) {
    const defaults = {
        theme: 'light',
        language: 'en',
        fontSize: 16,
        notifications: true
    };

    try {
        const stored = localStorage.getItem(`prefs_${userId}`);
        if (!stored) return defaults;

        const parsed = JSON.parse(stored);
        // Merge with defaults to fill any missing properties
        return { ...defaults, ...parsed };
    } catch (error) {
        console.warn('Failed to load preferences, using defaults.');
        return defaults;
    }
}

// Pattern 2: Feature detection with fallback
function getFormattedDate(date) {
    try {
        // Try modern Intl API
        return new Intl.DateTimeFormat('en-US', {
            weekday: 'long',
            year: 'numeric',
            month: 'long',
            day: 'numeric'
        }).format(date);
    } catch (error) {
        // Fallback to basic formatting
        return date.toDateString();
    }
}

// Pattern 3: Retry logic
function fetchWithRetry(url, maxRetries = 3) {
    let lastError;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            console.log(`Attempt ${attempt} of ${maxRetries}...`);
            // Simulating a fetch operation
            const response = makeRequest(url);
            return response; // Success, return immediately
        } catch (error) {
            lastError = error;
            console.warn(`Attempt ${attempt} failed: ${error.message}`);

            if (attempt < maxRetries) {
                console.log('Retrying...');
            }
        }
    }

    // All retries failed
    throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}

Input Validation with Errors

One of the most common uses of error handling is validating user input. By throwing descriptive errors during validation, you can provide clear feedback about what went wrong and how to fix it.

Example: Comprehensive Input Validation

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

function validateRegistrationForm(formData) {
    const errors = [];

    // Validate name
    if (!formData.name || formData.name.trim().length === 0) {
        errors.push(new ValidationError(
            'Name is required.', 'name', formData.name
        ));
    } else if (formData.name.trim().length < 2) {
        errors.push(new ValidationError(
            'Name must be at least 2 characters.', 'name', formData.name
        ));
    }

    // Validate email
    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!formData.email) {
        errors.push(new ValidationError(
            'Email is required.', 'email', formData.email
        ));
    } else if (!emailPattern.test(formData.email)) {
        errors.push(new ValidationError(
            'Please enter a valid email address.', 'email', formData.email
        ));
    }

    // Validate password
    if (!formData.password) {
        errors.push(new ValidationError(
            'Password is required.', 'password', ''
        ));
    } else if (formData.password.length < 8) {
        errors.push(new ValidationError(
            'Password must be at least 8 characters.', 'password', ''
        ));
    }

    // Validate age
    const age = Number(formData.age);
    if (isNaN(age) || age < 13 || age > 120) {
        errors.push(new ValidationError(
            'Age must be a number between 13 and 120.', 'age', formData.age
        ));
    }

    if (errors.length > 0) {
        return { valid: false, errors };
    }

    return { valid: true, errors: [] };
}

// Usage
const result = validateRegistrationForm({
    name: 'A',
    email: 'not-an-email',
    password: '123',
    age: 'ten'
});

if (!result.valid) {
    result.errors.forEach(err => {
        console.log(`${err.field}: ${err.message}`);
    });
}
// "name: Name must be at least 2 characters."
// "email: Please enter a valid email address."
// "password: Password must be at least 8 characters."
// "age: Age must be a number between 13 and 120."

Real-World Error Handling Examples

Let us look at practical scenarios you will encounter as a developer and how to handle errors in each situation.

API Error Handling

Example: Handling API Responses

class APIError extends Error {
    constructor(message, status, endpoint) {
        super(message);
        this.name = 'APIError';
        this.status = status;
        this.endpoint = endpoint;
    }
}

async function fetchData(endpoint) {
    try {
        const response = await fetch(endpoint);

        if (!response.ok) {
            // HTTP error responses (4xx, 5xx) do not throw automatically
            if (response.status === 404) {
                throw new APIError(
                    'The requested resource was not found.',
                    404, endpoint
                );
            } else if (response.status === 401) {
                throw new APIError(
                    'You must be logged in to access this resource.',
                    401, endpoint
                );
            } else if (response.status === 403) {
                throw new APIError(
                    'You do not have permission to access this resource.',
                    403, endpoint
                );
            } else if (response.status >= 500) {
                throw new APIError(
                    'The server encountered an error. Please try again later.',
                    response.status, endpoint
                );
            } else {
                throw new APIError(
                    `Request failed with status ${response.status}.`,
                    response.status, endpoint
                );
            }
        }

        // Parse the response body
        try {
            const data = await response.json();
            return data;
        } catch (parseError) {
            throw new APIError(
                'The server returned an invalid response.',
                response.status, endpoint
            );
        }

    } catch (error) {
        if (error instanceof APIError) {
            throw error; // Rethrow our custom errors
        }

        // Network errors (no internet, DNS failure, CORS, etc.)
        if (error.name === 'TypeError' && error.message === 'Failed to fetch') {
            throw new APIError(
                'Network error. Please check your internet connection.',
                0, endpoint
            );
        }

        throw error; // Rethrow unknown errors
    }
}

// Using the function
async function loadUserProfile(userId) {
    try {
        const user = await fetchData(`/api/users/${userId}`);
        displayProfile(user);
    } catch (error) {
        if (error instanceof APIError) {
            switch (error.status) {
                case 404:
                    showMessage('User not found.');
                    break;
                case 401:
                    redirectToLogin();
                    break;
                case 0:
                    showMessage('No internet connection.');
                    break;
                default:
                    showMessage(error.message);
            }
        } else {
            showMessage('An unexpected error occurred.');
            console.error(error);
        }
    }
}

Form Processing

Example: Safe Form Processing

function processContactForm(formElement) {
    const errors = [];
    const result = {};

    try {
        // Extract and validate form data
        const formData = new FormData(formElement);

        const name = formData.get('name');
        const email = formData.get('email');
        const message = formData.get('message');

        // Validate each field
        if (!name || name.trim().length < 2) {
            errors.push('Please enter your full name (at least 2 characters).');
        } else {
            result.name = name.trim();
        }

        if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
            errors.push('Please enter a valid email address.');
        } else {
            result.email = email.trim().toLowerCase();
        }

        if (!message || message.trim().length < 10) {
            errors.push('Message must be at least 10 characters.');
        } else {
            result.message = message.trim();
        }

        if (errors.length > 0) {
            return { success: false, errors };
        }

        // Process the validated data
        return { success: true, data: result };

    } catch (error) {
        console.error('Form processing error:', error);
        return {
            success: false,
            errors: ['An unexpected error occurred. Please try again.']
        };
    }
}

// Display errors to the user
function handleFormSubmit(event) {
    event.preventDefault();

    const result = processContactForm(event.target);

    if (result.success) {
        console.log('Form data:', result.data);
        // Submit to server...
    } else {
        result.errors.forEach(error => {
            console.log('Validation error:', error);
        });
    }
}

Data Processing

Example: Processing a Data File

function processCSVData(csvString) {
    const results = [];
    const errors = [];

    try {
        const lines = csvString.split('\n').filter(line => line.trim());

        if (lines.length < 2) {
            throw new Error('CSV must have a header row and at least one data row.');
        }

        const headers = lines[0].split(',').map(h => h.trim());

        // Process each row individually -- one bad row should not stop others
        for (let i = 1; i < lines.length; i++) {
            try {
                const values = lines[i].split(',').map(v => v.trim());

                if (values.length !== headers.length) {
                    throw new Error(
                        `Expected ${headers.length} columns but got ${values.length}`
                    );
                }

                const row = {};
                headers.forEach((header, index) => {
                    row[header] = values[index];
                });

                results.push(row);
            } catch (rowError) {
                errors.push({
                    row: i + 1,
                    message: rowError.message,
                    rawData: lines[i]
                });
            }
        }

    } catch (error) {
        return {
            success: false,
            error: error.message,
            results: [],
            rowErrors: []
        };
    }

    return {
        success: true,
        results,
        rowErrors: errors,
        totalRows: results.length + errors.length,
        successfulRows: results.length,
        failedRows: errors.length
    };
}

// Test with sample data
const csv = `name,email,age
Alice,alice@test.com,25
Bob,bob@test.com
Charlie,charlie@test.com,30`;

const report = processCSVData(csv);
console.log(`Processed: ${report.successfulRows}/${report.totalRows}`);
console.log('Results:', report.results);
if (report.rowErrors.length > 0) {
    console.log('Errors:');
    report.rowErrors.forEach(err => {
        console.log(`  Row ${err.row}: ${err.message}`);
    });
}
// "Processed: 2/3"
// Row 3 (Bob) fails because it only has 2 columns instead of 3

Error Handling Best Practices

Following these best practices will make your error handling more effective and your applications more robust.

Example: Best Practices

// 1. Be specific about what you catch
// BAD: Catches everything, hides bugs
try {
    doSomethingComplex();
} catch (error) {
    console.log('Something went wrong.'); // What went wrong?
}

// GOOD: Handle specific errors, rethrow unexpected ones
try {
    doSomethingComplex();
} catch (error) {
    if (error instanceof ValidationError) {
        showValidationMessage(error.message);
    } else if (error instanceof NetworkError) {
        showOfflineMessage();
    } else {
        throw error; // Do not swallow unexpected errors
    }
}

// 2. Do not use try/catch for flow control
// BAD: Using exceptions for normal program flow
function findUser(id) {
    try {
        return users[id].name; // Throws if user does not exist
    } catch {
        return 'Unknown';
    }
}

// GOOD: Check conditions explicitly
function findUser(id) {
    if (users[id]) {
        return users[id].name;
    }
    return 'Unknown';
}

// 3. Provide context in error messages
// BAD: Vague error message
throw new Error('Invalid input.');

// GOOD: Specific, actionable error message
throw new Error(
    `Invalid email format: "${email}". Expected format: user@domain.com`
);

// 4. Clean up resources in finally
function readFile(path) {
    let fileHandle = null;
    try {
        fileHandle = openFile(path);
        return fileHandle.read();
    } catch (error) {
        throw new Error(`Failed to read file "${path}": ${error.message}`);
    } finally {
        if (fileHandle) {
            fileHandle.close(); // Always clean up
        }
    }
}

// 5. Log errors for debugging, show friendly messages to users
try {
    const result = processOrder(orderData);
} catch (error) {
    // For developers (in the console)
    console.error('Order processing failed:', {
        error: error.message,
        stack: error.stack,
        orderData
    });

    // For users (on screen)
    showUserMessage('We could not process your order. Please try again.');
}
Note: Error handling adds overhead to your code because the JavaScript engine must set up the infrastructure to catch exceptions. Avoid wrapping performance-critical code in unnecessary try...catch blocks. Instead, validate inputs upfront and use try...catch only around operations that can genuinely fail -- like parsing external data, network requests, or file system access.
Pro Tip: In production applications, consider using a centralized error handler that catches all unhandled errors. In the browser, you can listen for window.onerror and window.onunhandledrejection events. In Node.js, use process.on('uncaughtException') and process.on('unhandledRejection'). These global handlers serve as a safety net for errors that slip through your regular error handling.
Common Mistake: Silently swallowing errors by writing empty catch blocks like catch (error) {}. This is one of the worst patterns in JavaScript because it hides bugs and makes them nearly impossible to diagnose. If you genuinely expect and want to ignore an error, add a comment explaining why. Otherwise, always log the error or rethrow it.

Practice Exercise

Build a complete user registration system with robust error handling that includes the following: (1) Create a custom error hierarchy with a base AppError class and specialized subclasses for ValidationError, DuplicateError, and DatabaseError. Each error class should include a statusCode property and any additional context specific to the error type. (2) Write a registerUser function that validates all inputs (name, email, password, age), checks for duplicate email addresses against a simulated database array, and returns the created user or throws the appropriate custom error. (3) Write a processRegistration function that calls registerUser inside a try/catch/finally block, handles each error type differently (display validation errors inline, show a conflict message for duplicates, show a retry message for database errors), and always logs the attempt in a finally block. (4) Test your system with at least five different scenarios: a successful registration, a missing required field, an invalid email, a duplicate email, and a password that is too short. Verify that each scenario produces the correct error type and message.