Error Handling

45 min read Intermediate

Learn to gracefully handle errors in your JavaScript applications using try/catch, custom errors, and best practices for robust code.

Types of Errors

JavaScript has several built-in error types, each indicating a different kind of problem.

JavaScript
// SyntaxError - Code can't be parsed
// const x = ; // SyntaxError: Unexpected token ';'

// ReferenceError - Variable doesn't exist
// console.log(undefinedVar); // ReferenceError

// TypeError - Wrong type for operation
// null.toString(); // TypeError: Cannot read properties of null

// RangeError - Number out of valid range
// new Array(-1); // RangeError: Invalid array length

// URIError - Malformed URI
// decodeURIComponent('%'); // URIError

// Error object properties
try {
    throw new Error("Something went wrong");
} catch (error) {
    console.log(error.name);    // "Error"
    console.log(error.message); // "Something went wrong"
    console.log(error.stack);   // Stack trace (non-standard but widely supported)
}
Error Hierarchy

All error types inherit from Error: ErrorTypeError, ReferenceError, SyntaxError, RangeError, URIError, EvalError

Try...Catch...Finally

The try/catch statement handles exceptions gracefully without crashing your program.

JavaScript
// Basic try...catch
try {
    // Code that might throw an error
    const data = JSON.parse("invalid json");
} catch (error) {
    // Handle the error
    console.log("Parse error:", error.message);
}
// Program continues...

// try...catch...finally
function fetchData() {
    console.log("Opening connection...");
    try {
        // Simulating an error
        throw new Error("Network error");
    } catch (error) {
        console.log("Error:", error.message);
    } finally {
        // Always runs, even if there's an error
        console.log("Closing connection...");
    }
}

fetchData();
// "Opening connection..."
// "Error: Network error"
// "Closing connection..."

// finally runs even with return
function getValue() {
    try {
        return 1;
    } finally {
        console.log("Cleanup"); // Still runs!
    }
}
console.log(getValue());
// "Cleanup"
// 1

// Catch without variable (ES2019)
try {
    JSON.parse("invalid");
} catch {
    console.log("Parse failed");
}

// Nested try...catch
function processData(data) {
    try {
        try {
            const parsed = JSON.parse(data);
            return parsed.value.toUpperCase();
        } catch (parseError) {
            console.log("Inner catch:", parseError.message);
            throw new Error("Data processing failed");
        }
    } catch (outerError) {
        console.log("Outer catch:", outerError.message);
        return null;
    }
}

console.log(processData("invalid"));
// "Inner catch: ..."
// "Outer catch: Data processing failed"
// null

Throwing Errors

Use throw to create your own errors when something goes wrong.

JavaScript
// Throwing an Error object
function divide(a, b) {
    if (b === 0) {
        throw new Error("Cannot divide by zero");
    }
    return a / b;
}

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

// Throwing specific error types
function setAge(age) {
    if (typeof age !== "number") {
        throw new TypeError("Age must be a number");
    }
    if (age < 0 || age > 150) {
        throw new RangeError("Age must be between 0 and 150");
    }
    return age;
}

// Validation with errors
function validateUser(user) {
    if (!user) {
        throw new Error("User is required");
    }
    if (!user.name) {
        throw new Error("User name is required");
    }
    if (!user.email) {
        throw new Error("User email is required");
    }
    if (!user.email.includes("@")) {
        throw new Error("Invalid email format");
    }
    return true;
}

try {
    validateUser({ name: "John" });
} catch (error) {
    console.log("Validation failed:", error.message);
    // "Validation failed: User email is required"
}

// Re-throwing errors
function processFile(filename) {
    try {
        // Simulate file operation
        if (!filename.endsWith(".txt")) {
            throw new Error("Only .txt files supported");
        }
        // Process file...
    } catch (error) {
        console.log(`Error processing ${filename}`);
        throw error; // Re-throw for caller to handle
    }
}

Custom Error Classes

Create your own error types for more specific error handling.

JavaScript
// Custom error class
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;
    }
}

class AuthenticationError extends Error {
    constructor(message = "Authentication required") {
        super(message);
        this.name = "AuthenticationError";
    }
}

// Using custom errors
function validateEmail(email) {
    if (!email) {
        throw new ValidationError("Email is required", "email");
    }
    if (!email.includes("@")) {
        throw new ValidationError("Invalid email format", "email");
    }
}

// Handling different error types
function handleRequest(user) {
    try {
        if (!user.token) {
            throw new AuthenticationError();
        }
        validateEmail(user.email);
        // Make request...
    } catch (error) {
        if (error instanceof AuthenticationError) {
            console.log("Please log in first");
            // Redirect to login...
        } else if (error instanceof ValidationError) {
            console.log(`Invalid ${error.field}: ${error.message}`);
            // Show form error...
        } else if (error instanceof NetworkError) {
            console.log(`Network error (${error.statusCode}): ${error.message}`);
            // Retry logic...
        } else {
            // Unknown error - re-throw or log
            console.log("Unexpected error:", error);
            throw error;
        }
    }
}

// HTTP-style errors
class HttpError extends Error {
    constructor(status, message) {
        super(message);
        this.name = "HttpError";
        this.status = status;
    }
    
    static badRequest(message = "Bad Request") {
        return new HttpError(400, message);
    }
    
    static unauthorized(message = "Unauthorized") {
        return new HttpError(401, message);
    }
    
    static notFound(message = "Not Found") {
        return new HttpError(404, message);
    }
    
    static internal(message = "Internal Server Error") {
        return new HttpError(500, message);
    }
}

// Usage
throw HttpError.notFound("User not found");

Error Handling Patterns

Common patterns for handling errors effectively.

JavaScript
// Pattern 1: Error-first callbacks (Node.js style)
function readFileCallback(filename, callback) {
    setTimeout(() => {
        if (!filename) {
            callback(new Error("Filename required"), null);
        } else {
            callback(null, "File contents here");
        }
    }, 100);
}

readFileCallback("test.txt", (error, data) => {
    if (error) {
        console.log("Error:", error.message);
        return;
    }
    console.log("Data:", data);
});

// Pattern 2: Result objects (no exceptions)
function safeDivide(a, b) {
    if (b === 0) {
        return { success: false, error: "Cannot divide by zero" };
    }
    return { success: true, value: a / b };
}

const result = safeDivide(10, 0);
if (result.success) {
    console.log("Result:", result.value);
} else {
    console.log("Error:", result.error);
}

// Pattern 3: Optional chaining for safe access
const user = {
    profile: null
};

// Old way
const city = user && user.profile && user.profile.address && user.profile.address.city;

// Modern way - no errors even if null
const cityModern = user?.profile?.address?.city;
console.log(cityModern); // undefined (not an error)

// Pattern 4: Nullish coalescing for defaults
const name = user?.name ?? "Anonymous";
console.log(name); // "Anonymous"

// Pattern 5: Wrapper functions for try/catch
function tryCatch(fn) {
    return function(...args) {
        try {
            return { success: true, value: fn(...args) };
        } catch (error) {
            return { success: false, error };
        }
    };
}

const safeParseJSON = tryCatch(JSON.parse);

console.log(safeParseJSON('{"valid": true}'));
// { success: true, value: { valid: true } }

console.log(safeParseJSON('invalid'));
// { success: false, error: SyntaxError }

// Pattern 6: Async error boundary
async function withErrorBoundary(fn) {
    try {
        return await fn();
    } catch (error) {
        console.error("Error caught:", error.message);
        // Log to service, show notification, etc.
        return null;
    }
}

Debugging Tips

Tools and techniques for finding and fixing errors.

JavaScript
// Console methods for debugging
console.log("Basic log");
console.error("Error log");   // Red in console
console.warn("Warning log");  // Yellow in console
console.info("Info log");
console.debug("Debug log");

// Grouping logs
console.group("User Details");
console.log("Name: John");
console.log("Age: 30");
console.groupEnd();

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

// Timing
console.time("operation");
// ... some code ...
console.timeEnd("operation"); // "operation: 123ms"

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

// Assert (logs only if condition is false)
console.assert(1 === 2, "Math is broken!");

// Debugger statement - pauses execution
function buggyFunction(x) {
    debugger; // Opens browser dev tools here
    return x * 2;
}

// Inspecting error stack
function level3() {
    throw new Error("Deep error");
}
function level2() { level3(); }
function level1() { level2(); }

try {
    level1();
} catch (error) {
    console.log(error.stack);
    // Error: Deep error
    //   at level3 (...)
    //   at level2 (...)
    //   at level1 (...)
}

// Error cause (ES2022)
try {
    try {
        throw new Error("Database connection failed");
    } catch (dbError) {
        throw new Error("User fetch failed", { cause: dbError });
    }
} catch (error) {
    console.log(error.message); // "User fetch failed"
    console.log(error.cause.message); // "Database connection failed"
}

Best Practices

JavaScript
// 1. Be specific about what you catch
// Bad - catches everything
try {
    doSomething();
} catch (e) {
    console.log("Something went wrong");
}

// Good - handle specific errors
try {
    doSomething();
} catch (error) {
    if (error instanceof NetworkError) {
        // Handle network issues
    } else if (error instanceof ValidationError) {
        // Handle validation
    } else {
        throw error; // Re-throw unknown errors
    }
}

// 2. Don't swallow errors silently
// Bad
try {
    riskyOperation();
} catch (e) {
    // Empty catch - error disappears!
}

// Good - at minimum, log the error
try {
    riskyOperation();
} catch (error) {
    console.error("Operation failed:", error);
    // Consider: report to error tracking service
}

// 3. Fail fast with validation
function processOrder(order) {
    // Validate early
    if (!order) throw new Error("Order is required");
    if (!order.items?.length) throw new Error("Order must have items");
    if (order.total < 0) throw new Error("Invalid order total");
    
    // Now process with confidence
    return processValidOrder(order);
}

// 4. Provide context in error messages
// Bad
throw new Error("Invalid input");

// Good
throw new Error(`Invalid user ID: ${userId}. Expected positive integer.`);

// 5. Clean up resources in finally
function processWithResource() {
    const connection = openConnection();
    try {
        return doWork(connection);
    } finally {
        connection.close(); // Always runs
    }
}

// 6. Use error boundaries in UI frameworks
class ErrorBoundary {
    constructor(fallback) {
        this.fallback = fallback;
        this.error = null;
    }
    
    wrap(fn) {
        try {
            return fn();
        } catch (error) {
            this.error = error;
            return this.fallback(error);
        }
    }
}
Error Handling Checklist
  • Validate inputs before processing
  • Use descriptive error messages
  • Log errors with context (user, request, timestamp)
  • Don't expose sensitive info in production errors
  • Have a global error handler as fallback
  • Test error scenarios, not just happy paths

Advanced Custom Error Patterns

Build a comprehensive error handling system with error codes, metadata, and proper serialization.

JavaScript
// Base application error with rich metadata
class AppError extends Error {
    constructor(message, options = {}) {
        super(message, { cause: options.cause });
        this.name = this.constructor.name;
        this.code = options.code || "UNKNOWN_ERROR";
        this.statusCode = options.statusCode || 500;
        this.isOperational = options.isOperational ?? true;
        this.metadata = options.metadata || {};
        this.timestamp = new Date().toISOString();
        
        Error.captureStackTrace?.(this, this.constructor);
    }
    
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            code: this.code,
            statusCode: this.statusCode,
            metadata: this.metadata,
            timestamp: this.timestamp,
            ...(process.env.NODE_ENV !== "production" && { stack: this.stack })
        };
    }
}

// Specific error types
class ValidationError extends AppError {
    constructor(message, field, options = {}) {
        super(message, { 
            ...options,
            code: options.code || "VALIDATION_ERROR",
            statusCode: 400 
        });
        this.field = field;
        this.metadata.field = field;
    }
}

class NotFoundError extends AppError {
    constructor(resource, id, options = {}) {
        super(`${resource} with id '${id}' not found`, {
            ...options,
            code: "NOT_FOUND",
            statusCode: 404,
            metadata: { resource, id }
        });
    }
}

class AuthenticationError extends AppError {
    constructor(message = "Authentication required", options = {}) {
        super(message, {
            ...options,
            code: "UNAUTHENTICATED",
            statusCode: 401
        });
    }
}

class AuthorizationError extends AppError {
    constructor(message = "Access denied", options = {}) {
        super(message, {
            ...options,
            code: "FORBIDDEN",
            statusCode: 403
        });
    }
}

// Usage
try {
    throw new NotFoundError("User", "abc123");
} catch (error) {
    console.log(error.toJSON());
    // { name: "NotFoundError", message: "User with id 'abc123' not found", 
    //   code: "NOT_FOUND", statusCode: 404, metadata: { resource: "User", id: "abc123" }, ... }
}

// Error codes enum
const ErrorCodes = Object.freeze({
    VALIDATION: "VALIDATION_ERROR",
    NOT_FOUND: "NOT_FOUND",
    DUPLICATE: "DUPLICATE_ENTRY",
    RATE_LIMIT: "RATE_LIMIT_EXCEEDED",
    NETWORK: "NETWORK_ERROR",
    TIMEOUT: "REQUEST_TIMEOUT"
});

Error Boundaries and Recovery

Implement error boundaries to prevent cascading failures and enable graceful recovery.

JavaScript
// Generic error boundary wrapper
function createErrorBoundary(options = {}) {
    const { 
        fallback, 
        onError, 
        retries = 0,
        retryDelay = 1000 
    } = options;
    
    return async function boundary(fn) {
        let lastError;
        
        for (let attempt = 0; attempt <= retries; attempt++) {
            try {
                return await fn();
            } catch (error) {
                lastError = error;
                onError?.(error, attempt);
                
                if (attempt < retries) {
                    await new Promise(r => setTimeout(r, retryDelay * (attempt + 1)));
                }
            }
        }
        
        if (fallback) {
            return typeof fallback === "function" ? fallback(lastError) : fallback;
        }
        
        throw lastError;
    };
}

// Usage
const safeFetch = createErrorBoundary({
    retries: 3,
    retryDelay: 1000,
    onError: (err, attempt) => console.log(`Attempt ${attempt + 1} failed:`, err.message),
    fallback: (err) => ({ error: true, message: err.message, data: null })
});

const result = await safeFetch(async () => {
    const response = await fetch("/api/data");
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
});

// Circuit breaker pattern
class CircuitBreaker {
    constructor(options = {}) {
        this.failureThreshold = options.failureThreshold || 5;
        this.resetTimeout = options.resetTimeout || 30000;
        this.state = "CLOSED";  // CLOSED, OPEN, HALF_OPEN
        this.failures = 0;
        this.lastFailure = null;
    }
    
    async execute(fn) {
        if (this.state === "OPEN") {
            if (Date.now() - this.lastFailure > this.resetTimeout) {
                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.lastFailure = Date.now();
        
        if (this.failures >= this.failureThreshold) {
            this.state = "OPEN";
        }
    }
}

// Global unhandled error handlers
window.addEventListener("error", (event) => {
    console.error("Uncaught error:", event.error);
    // Report to error tracking service
    reportError(event.error);
});

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

// Node.js equivalents
process.on("uncaughtException", (error) => {
    console.error("Uncaught exception:", error);
    // Graceful shutdown
    process.exit(1);
});

process.on("unhandledRejection", (reason, promise) => {
    console.error("Unhandled rejection at:", promise, "reason:", reason);
});

Error Logging Strategies

Implement structured logging for effective debugging and monitoring in production.

JavaScript
// Structured logger
class Logger {
    static levels = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
    
    constructor(options = {}) {
        this.minLevel = Logger.levels[options.level || "INFO"];
        this.context = options.context || {};
    }
    
    #log(level, message, data = {}) {
        if (Logger.levels[level] < this.minLevel) return;
        
        const entry = {
            timestamp: new Date().toISOString(),
            level,
            message,
            ...this.context,
            ...data
        };
        
        const formatted = JSON.stringify(entry);
        
        switch (level) {
            case "ERROR": console.error(formatted); break;
            case "WARN": console.warn(formatted); break;
            default: console.log(formatted);
        }
        
        return entry;
    }
    
    debug(msg, data) { return this.#log("DEBUG", msg, data); }
    info(msg, data) { return this.#log("INFO", msg, data); }
    warn(msg, data) { return this.#log("WARN", msg, data); }
    error(msg, data) { return this.#log("ERROR", msg, data); }
    
    child(context) {
        return new Logger({
            level: Object.keys(Logger.levels)[this.minLevel],
            context: { ...this.context, ...context }
        });
    }
}

// Usage
const logger = new Logger({ 
    level: "DEBUG",
    context: { service: "user-api", version: "1.0.0" }
});

const requestLogger = logger.child({ requestId: "abc123" });

try {
    throw new ValidationError("Email is invalid", "email");
} catch (error) {
    requestLogger.error("Validation failed", {
        error: {
            name: error.name,
            message: error.message,
            code: error.code,
            field: error.field,
            stack: error.stack
        },
        userId: "user123",
        action: "createUser"
    });
}

// Error tracking service integration
class ErrorTracker {
    constructor(apiKey, options = {}) {
        this.apiKey = apiKey;
        this.endpoint = options.endpoint || "/api/errors";
        this.environment = options.environment || "development";
    }
    
    capture(error, context = {}) {
        const payload = {
            name: error.name,
            message: error.message,
            stack: error.stack,
            code: error.code,
            timestamp: new Date().toISOString(),
            environment: this.environment,
            url: window?.location?.href,
            userAgent: navigator?.userAgent,
            ...context
        };
        
        // Send async, don't block
        this.#send(payload).catch(console.error);
    }
    
    async #send(payload) {
        await fetch(this.endpoint, {
            method: "POST",
            headers: { 
                "Content-Type": "application/json",
                "X-API-Key": this.apiKey 
            },
            body: JSON.stringify(payload)
        });
    }
}

// Initialize error tracking
const errorTracker = new ErrorTracker("your-api-key", {
    environment: process.env.NODE_ENV
});

// Wrap functions with automatic error tracking
function withErrorTracking(fn, context = {}) {
    return async (...args) => {
        try {
            return await fn(...args);
        } catch (error) {
            errorTracker.capture(error, {
                ...context,
                functionName: fn.name,
                arguments: args
            });
            throw error;
        }
    };
}

Production Error Handling

  • Use error tracking: Sentry, Bugsnag, or LogRocket
  • Structured logging: JSON format for parsing/querying
  • Correlation IDs: Track errors across services
  • Sanitize: Never log passwords, tokens, PII
  • Alert: Set up alerts for error spikes

Summary

try...catch

Wrap risky code, handle errors gracefully

finally

Always runs for cleanup operations

throw

Create and throw your own errors

Custom Errors

Extend Error for specific error types

Error Properties

name, message, stack, cause

Best Practice

Be specific, don't swallow, provide context