advanced Step 14 of 20

Error Handling

JavaScript Programming

Error Handling

Robust error handling separates professional-quality code from fragile scripts that break unexpectedly. JavaScript provides the try/catch/finally mechanism for handling runtime errors, along with the ability to create custom error classes for domain-specific error types. In modern JavaScript, error handling extends to asynchronous code with async/await and Promises. Understanding how to anticipate, catch, and recover from errors makes your applications more reliable and provides better user experiences when things go wrong.

Try/Catch/Finally

// Basic try/catch
try {
    const data = JSON.parse('{"valid": true}');
    console.log(data.valid);  // true
} catch (error) {
    console.error("Parse error:", error.message);
}

// Finally — always executes
function readFile(path) {
    let connection = null;
    try {
        connection = openConnection(path);
        return connection.read();
    } catch (error) {
        console.error("Read failed:", error.message);
        return null;
    } finally {
        // Cleanup — runs even if return was called
        if (connection) connection.close();
        console.log("Connection cleanup complete");
    }
}

// Catching specific error types
try {
    undefinedFunction();
} catch (error) {
    if (error instanceof ReferenceError) {
        console.log("Variable/function not found");
    } else if (error instanceof TypeError) {
        console.log("Wrong type operation");
    } else if (error instanceof SyntaxError) {
        console.log("Syntax issue");
    } else {
        throw error;  // Re-throw unknown errors
    }
}

Throwing Errors

// Throw built-in error types
function divide(a, b) {
    if (typeof a !== "number" || typeof b !== "number") {
        throw new TypeError("Both arguments must be numbers");
    }
    if (b === 0) {
        throw new RangeError("Cannot divide by zero");
    }
    return a / b;
}

try {
    console.log(divide(10, 0));
} catch (e) {
    console.error(`${e.constructor.name}: ${e.message}`);
    // "RangeError: Cannot divide by zero"
}

// Validation function
function validateEmail(email) {
    if (!email) throw new Error("Email is required");
    if (!email.includes("@")) throw new Error("Invalid email format");
    return true;
}

Custom Error Classes

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

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

class NotFoundError extends AppError {
    constructor(resource) {
        super(`${resource} not found`, 404);
        this.name = "NotFoundError";
        this.resource = resource;
    }
}

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

// Using custom errors
function getUser(id) {
    if (!id) throw new ValidationError("id", "User ID is required");
    const user = database.find(u => u.id === id);
    if (!user) throw new NotFoundError("User");
    return user;
}

try {
    const user = getUser(null);
} catch (error) {
    if (error instanceof ValidationError) {
        console.log(`Validation: ${error.field} - ${error.message}`);
    } else if (error instanceof NotFoundError) {
        console.log(`Not found: ${error.resource}`);
    } else {
        console.error("Unexpected error:", error);
    }
}

Async Error Handling

// With async/await
async function fetchUserData(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
            throw new AppError("Failed to fetch user", response.status);
        }
        return await response.json();
    } catch (error) {
        if (error instanceof AppError) {
            console.error(`App error (${error.statusCode}): ${error.message}`);
        } else {
            console.error("Network error:", error.message);
        }
        return null;
    }
}

// Global error handlers
window.addEventListener("unhandledrejection", (event) => {
    console.error("Unhandled promise rejection:", event.reason);
    event.preventDefault();  // Prevent default browser behavior
});

window.addEventListener("error", (event) => {
    console.error("Uncaught error:", event.error);
    // Log to error tracking service
});
Pro tip: Create a hierarchy of custom error classes for your application. Have a base AppError class and specific subclasses like ValidationError, NotFoundError, and AuthError. This makes error handling in your catch blocks more precise and allows middleware to handle different error types appropriately.

Key Takeaways

  • Use try/catch/finally to handle errors gracefully; finally always runs for cleanup.
  • Throw meaningful errors with throw new Error("message") and specific error types.
  • Create custom error classes extending Error for domain-specific error handling.
  • Handle async errors with try/catch inside async functions and .catch() on Promises.
  • Set up global error handlers (unhandledrejection, error events) to catch any errors that slip through.