13. Error Handling

Types of Errors

Syntax Errors

// These occur during parsing/compilation
// console.log("Hello World") // Missing semicolon - SyntaxError

Runtime Errors

// These occur during execution
// console.log(undefinedVariable); // ReferenceError
// console.log("Hello".toUpperCase()); // This works
// (42).toUpperCase(); // TypeError

Logical Errors

// These are bugs in logic that don't throw errors
function add(a, b) {
    return a - b; // Should be +, but no error thrown
}

try...catch Statement

try {
    // Code that might throw an error
    const result = riskyOperation();
    console.log("Success:", result);
} catch (error) {
    // Handle the error
    console.log("Error occurred:", error.message);
} finally {
    // Always executes
    console.log("Cleanup code here");
}

Error Objects

Built-in Error Types

// ReferenceError
try {
    console.log(undefinedVariable);
} catch (error) {
    console.log(error.name);    // "ReferenceError"
    console.log(error.message); // "undefinedVariable is not defined"
}

// TypeError
try {
    null.toString();
} catch (error) {
    console.log(error.name);    // "TypeError"
    console.log(error.message); // "Cannot read property 'toString' of null"
}

// SyntaxError (usually caught at parse time)
// const obj = { a: 1, b: 2, }; // Trailing comma is OK in objects

// RangeError
try {
    const arr = new Array(-1);
} catch (error) {
    console.log(error.name);    // "RangeError"
    console.log(error.message); // "Invalid array length"
}

// URIError
try {
    decodeURIComponent("%");
} catch (error) {
    console.log(error.name);    // "URIError"
    console.log(error.message); // "URI malformed"
}

Custom Errors

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

class NetworkError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.name = "NetworkError";
        this.statusCode = statusCode;
    }
}

function validateUser(user) {
    if (!user.name) {
        throw new ValidationError("Name is required", "name");
    }
    if (user.age < 0) {
        throw new ValidationError("Age cannot be negative", "age");
    }
}

try {
    validateUser({ name: "", age: -5 });
} catch (error) {
    if (error instanceof ValidationError) {
        console.log(`Validation failed for ${error.field}: ${error.message}`);
    } else {
        console.log("Unexpected error:", error.message);
    }
}

throw Statement

function divide(a, b) {
    if (b === 0) {
        throw new Error("Division by zero is not allowed");
    }
    if (typeof a !== "number" || typeof b !== "number") {
        throw new TypeError("Both arguments must be numbers");
    }
    return a / b;
}

try {
    console.log(divide(10, 0));
} catch (error) {
    console.log("Error:", error.message);
}

try {
    console.log(divide(10, "2"));
} catch (error) {
    console.log("Type error:", error.message);
}

Error Propagation

function level1() {
    level2();
}

function level2() {
    level3();
}

function level3() {
    throw new Error("Something went wrong in level3");
}

try {
    level1();
} catch (error) {
    console.log("Caught error:", error.message);
    console.log("Stack trace:", error.stack);
}

finally Block

function readFile(fileName) {
    let fileHandle;
    try {
        fileHandle = openFile(fileName);
        const content = readContent(fileHandle);
        return content;
    } catch (error) {
        console.log("Error reading file:", error.message);
        throw error; // Re-throw the error
    } finally {
        // Always close the file, even if an error occurred
        if (fileHandle) {
            closeFile(fileHandle);
            console.log("File closed");
        }
    }
}

Async Error Handling

Promises

function fetchUserData(userId) {
    return fetch(`/api/users/${userId}`)
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            return response.json();
        })
        .catch(error => {
            console.log("Fetch error:", error.message);
            throw error; // Re-throw to propagate
        });
}

// Usage
fetchUserData(123)
    .then(user => console.log(user))
    .catch(error => console.log("Final error:", error.message));

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();
        return userData;
    } catch (error) {
        console.log("Error fetching user data:", error.message);
        throw error;
    }
}

// Usage
async function main() {
    try {
        const user = await fetchUserData(123);
        console.log(user);
    } catch (error) {
        console.log("Failed to get user data:", error.message);
    }
}

main();

Best Practices

1. Use Specific Error Types

class DatabaseError extends Error {
    constructor(message, code) {
        super(message);
        this.name = "DatabaseError";
        this.code = code;
    }
}

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

2. Don't Ignore Errors

// Bad
try {
    riskyOperation();
} catch (error) {
    // Ignoring the error
}

// Good
try {
    riskyOperation();
} catch (error) {
    console.error("Operation failed:", error);
    // Handle the error appropriately
}

3. Use finally for Cleanup

function processFile(fileName) {
    let file;
    try {
        file = openFile(fileName);
        const data = processData(file);
        saveResults(data);
    } finally {
        if (file) {
            file.close();
        }
    }
}

4. Handle Errors at the Right Level

// Low-level function
function parseJSON(jsonString) {
    try {
        return JSON.parse(jsonString);
    } catch (error) {
        throw new Error(`Invalid JSON: ${error.message}`);
    }
}

// High-level function
function loadUserConfig() {
    try {
        const configText = readConfigFile();
        const config = parseJSON(configText);
        return config;
    } catch (error) {
        // Provide user-friendly error message
        console.error("Failed to load user configuration:", error.message);
        return getDefaultConfig();
    }
}

5. Log Errors Appropriately

function handleError(error, context = {}) {
    const errorInfo = {
        message: error.message,
        stack: error.stack,
        timestamp: new Date().toISOString(),
        context: context
    };

    // Log to console in development
    if (process.env.NODE_ENV === 'development') {
        console.error('Error occurred:', errorInfo);
    }

    // Send to error reporting service in production
    // errorReportingService.send(errorInfo);
}

Next Steps

Proper error handling makes your code more robust and maintainable. Next, let's explore asynchronous programming, which is crucial for modern web development.