Skip to content

JavaScript Error Handling

Mattscreative edited this page Feb 21, 2026 · 1 revision

JavaScript Error Handling and Debugging Guide

Table of Contents


Introduction

Error handling is crucial for building robust applications. Proper error handling helps you identify bugs, recover from failures, and provide good user experiences.


Error Types

Built-in Error Types

// Error - base error object
const error = new Error("Something went wrong");
console.log(error.message);  // "Something went wrong"
console.log(error.name);     // "Error"
console.log(error.stack);    // Stack trace

// TypeError - when value is not expected type
const obj = {};
obj();  // TypeError: obj is not a function

// ReferenceError - using undefined variable
console.log(undefinedVariable);  // ReferenceError

// SyntaxError - invalid syntax (can't catch in same file)
JSON.parse("{invalid}");  // SyntaxError

// RangeError - value outside range
const arr = new Array(-1);  // RangeError: Invalid array length

// URIError - invalid URI
decodeURIComponent("%");  // URIError

// EvalError - error in eval()
eval("invalid syntax");  // EvalError

Checking Error Types

try {
    someFunction();
} catch (error) {
    if (error instanceof TypeError) {
        console.log("Type error:", error.message);
    } else if (error instanceof ReferenceError) {
        console.log("Reference error:", error.message);
    } else if (error instanceof RangeError) {
        console.log("Range error:", error.message);
    } else if (error instanceof SyntaxError) {
        console.log("Syntax error:", error.message);
    } else {
        throw error;  // Re-throw unknown errors
    }
}

Try/Catch/Finally

Basic Try/Catch

// Basic syntax
try {
    // Code that might throw an error
    const result = riskyOperation();
    console.log("Result:", result);
} catch (error) {
    // Handle the error
    console.error("Error occurred:", error.message);
}

// Without error - skip catch
try {
    const x = 5;
    console.log("Success:", x);
} catch (error) {
    console.log("This won't run");
}

// With error
try {
    throw new Error("Oops!");
} catch (error) {
    console.log("Caught:", error.message);  // "Caught: Oops!"
}

Try/Catch/Finally

// finally always runs
try {
    console.log("In try");
    throw new Error("Error!");
} catch (error) {
    console.log("In catch:", error.message);
} finally {
    console.log("In finally - cleanup here");
}

// finally runs even with return
function example() {
    try {
        return "from try";
    } finally {
        console.log("finally runs");
    }
}

// finally runs even with throw
function example2() {
    try {
        throw new Error("Error!");
    } finally {
        console.log("finally still runs");
    }
}

Nested Try/Catch

try {
    try {
        throw new Error("Inner error");
    } catch (innerError) {
        console.log("Caught inner:", innerError.message);
        throw new Error("Outer error");
    }
} catch (outerError) {
    console.log("Caught outer:", outerError.message);
}

Throwing Errors

Throwing Built-in Errors

// Throw error
throw new Error("Something went wrong");

// Throw other types (not recommended)
throw "Error message";  // String - bad practice
throw 42;               // Number - bad practice

// Conditional throwing
function divide(a, b) {
    if (b === 0) {
        throw new Error("Cannot divide by zero");
    }
    return a / b;
}

try {
    divide(10, 0);
} catch (error) {
    console.log(error.message);  // "Cannot divide by zero"
}

Error Object Properties

const error = new Error("Custom message");

// Properties available
console.log(error.name);     // "Error"
console.log(error.message);  // "Custom message"
console.log(error.stack);    // Full stack trace
console.log(error.cause);    // ES2022 - original error (if passed)

Custom Errors

Creating Custom Error Classes

// Custom error class
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
        // Maintains proper stack trace
        Error.captureStackTrace(this, this.constructor);
    }
}

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

// Usage
function getUser(id) {
    const user = database.find(id);
    if (!user) {
        throw new NotFoundError("User");
    }
    return user;
}

try {
    getUser(999);
} catch (error) {
    if (error instanceof NotFoundError) {
        console.log(error.message);  // "User not found"
    }
}

Advanced Custom Errors

// Error with additional properties
class APIError extends Error {
    constructor(message, statusCode, details = {}) {
        super(message);
        this.name = "APIError";
        this.statusCode = statusCode;
        this.details = details;
        Error.captureStackTrace(this, this.constructor);
    }
}

// Error with cause
try {
    try {
        fetchData();
    } catch (error) {
        // Wrap with additional context
        throw new Error("Failed to load user data", { cause: error });
    }
} catch (error) {
    console.log(error.cause);  // Original error
}

Async Error Handling

Try/Catch with Async/Await

// Basic async error handling
async function fetchData(url) {
    try {
        const response = await fetch(url);
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        console.error("Fetch failed:", error.message);
        throw error;
    }
}

// Using the function
async function main() {
    try {
        const data = await fetchData("https://api.example.com/data");
        console.log("Data:", data);
    } catch (error) {
        console.error("Failed to fetch data:", error.message);
    }
}

Promise Error Handling

// .catch() handler
fetch("https://api.example.com/data")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error("Error:", error.message));

// .catch() with multiple errors
Promise.all([
    fetch("/api/users").then(r => r.json()),
    fetch("/api/posts").then(r => r.json())
])
    .then(([users, posts]) => console.log(users, posts))
    .catch(error => console.error("At least one request failed:", error));

Error Handling in Promise.all

// Promise.all - fails fast if any promise rejects
async function fetchAll() {
    try {
        const results = await Promise.all([
            fetch("/api/data1").then(r => r.json()),
            fetch("/api/data2").then(r => r.json())
        ]);
        return results;
    } catch (error) {
        console.error("One or more requests failed");
        throw error;
    }
}

// Promise.allSettled - never rejects
async function fetchAllSafe() {
    const results = await Promise.allSettled([
        fetch("/api/data1").then(r => r.json()),
        fetch("/api/data2").then(r => r.json())
    ]);
    
    const successful = results
        .filter(r => r.status === "fulfilled")
        .map(r => r.value);
    
    const failed = results
        .filter(r => r.status === "rejected")
        .map(r => r.reason);
    
    return { successful, failed };
}

Debugging Techniques

Console Methods

// Different console methods
console.log("Info message");
console.info("Information");
console.warn("Warning");
console.error("Error");

// Console with formatting
console.log("User: %s, Age: %d", "Alice", 25);
console.log("Object: %o", { a: 1, b: 2 });

// Grouping console output
console.group("User Details");
console.log("Name: Alice");
console.log("Age: 25");
console.groupEnd();

// Table output (for arrays/objects)
const users = [
    { name: "Alice", age: 25 },
    { name: "Bob", age: 30 }
];
console.table(users);

// Timing
console.time("fetch");
await fetchData();
console.timeEnd("fetch");

// Stack trace
console.trace("Stack trace");

Breakpoints

// Debugger keyword - pauses execution
function processData(data) {
    debugger;  // Execution pauses here in DevTools
    // inspect variables
    return data.map(item => item * 2);
}

// Conditional breakpoints (set in DevTools)
// Right-click line number > Add conditional breakpoint
function complexFunction(x, y) {
    const result = x * y;
    return result;
}

Console Debugging

// Inspect objects
const obj = { a: 1, b: 2, c: 3 };
console.log(obj);
console.dir(obj);  // Interactive tree view

// Clear console
console.clear();

// Assert (only logs if false)
const x = 5;
console.assert(x > 10, "x should be greater than 10");

Logging Best Practices

Structured Logging

// Log levels
const LOG_LEVELS = {
    ERROR: 0,
    WARN: 1,
    INFO: 2,
    DEBUG: 3
};

function log(level, message, meta = {}) {
    if (level <= LOG_LEVELS.DEBUG) {  // Set current level
        console[level === LOG_LEVELS.ERROR ? "error" : 
                level === LOG_LEVELS.WARN ? "warn" : 
                "log"]({
            timestamp: new Date().toISOString(),
            level: Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k] === level),
            message,
            ...meta
        });
    }
}

log(LOG_LEVELS.INFO, "User logged in", { userId: 123 });
log(LOG_LEVELS.ERROR, "Payment failed", { error: "Insufficient funds", amount: 100 });

Error Logging

// Capture and log errors
window.addEventListener("error", (event) => {
    console.error("Global error:", event.error);
});

// Log async errors
window.addEventListener("unhandledrejection", (event) => {
    console.error("Unhandled promise rejection:", event.reason);
});

// Send to error tracking service
function logError(error, context = {}) {
    const errorData = {
        message: error.message,
        stack: error.stack,
        url: window.location.href,
        timestamp: new Date().toISOString(),
        userAgent: navigator.userAgent,
        ...context
    };
    
    // Send to server or error tracking service
    console.log("Error logged:", errorData);
}

Error Recovery Patterns

Graceful Degradation

// Provide fallback when feature fails
async function getUserData() {
    try {
        const user = await fetchUser();
        const preferences = await fetchPreferences();
        return { user, preferences };
    } catch (error) {
        console.warn("Failed to load full data:", error.message);
        // Return partial data
        try {
            const user = await fetchUser();
            return { user, preferences: {} };
        } catch (userError) {
            return { user: null, preferences: {} };
        }
    }
}

// Default values pattern
function processInput(input) {
    const config = {
        timeout: 5000,
        retries: 3,
        ...input
    };
    
    return config;
}

Retry Pattern

// Retry with exponential backoff
async function retry(fn, maxAttempts = 3, delay = 1000) {
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            return await fn();
        } catch (error) {
            if (attempt === maxAttempts) {
                throw error;
            }
            
            const waitTime = delay * Math.pow(2, attempt - 1);
            console.warn(`Attempt ${attempt} failed, retrying in ${waitTime}ms`);
            await new Promise(r => setTimeout(r, waitTime));
        }
    }
}

// Usage
const data = await retry(() => fetchData());

Circuit Breaker

class CircuitBreaker {
    constructor(failureThreshold = 5, timeout = 60000) {
        this.failureThreshold = failureThreshold;
        this.timeout = timeout;
        this.failures = 0;
        this.lastFailureTime = null;
        this.state = "CLOSED";  // CLOSED, OPEN, HALF_OPEN
    }
    
    async execute(fn) {
        if (this.state === "OPEN") {
            if (Date.now() - this.lastFailureTime > this.timeout) {
                this.state = "HALF_OPEN";
            } else {
                throw new Error("Circuit breaker is OPEN");
            }
        }
        
        try {
            const result = await fn();
            this.onSuccess();
            return result;
        } catch (error) {
            this.onFailure();
            throw error;
        }
    }
    
    onSuccess() {
        this.failures = 0;
        this.state = "CLOSED";
    }
    
    onFailure() {
        this.failures++;
        this.lastFailureTime = Date.now();
        
        if (this.failures >= this.failureThreshold) {
            this.state = "OPEN";
        }
    }
}

Testing for Errors

Jest Testing

// Test that function throws
function divide(a, b) {
    if (b === 0) throw new Error("Cannot divide by zero");
    return a / b;
}

// Using expect().toThrow()
test("divide throws on zero", () => {
    expect(() => divide(10, 0)).toThrow();
    expect(() => divide(10, 0)).toThrow("Cannot divide by zero");
});

// Test async errors
test("fetchData throws on error", async () => {
    await expect(fetchData("invalid-url")).rejects.toThrow();
});

// Test custom errors
test("throws ValidationError for invalid input", () => {
    expect(() => validateInput("")).toThrow(ValidationError);
});

Quick Reference

// Try/catch
try {
    riskyOperation();
} catch (error) {
    console.error(error.message);
} finally {
    cleanup();
}

// Throw error
throw new Error("Message");

// Custom error
class MyError extends Error {
    constructor(message) {
        super(message);
        this.name = "MyError";
    }
}

// Async error handling
async function main() {
    try {
        const data = await fetchData();
    } catch (error) {
        console.error(error);
    }
}

// Promise error handling
fetchData()
    .catch(error => console.error(error));

Clone this wiki locally