Lesson 10 • Intermediate

    Error Handling

    Errors are not signs of failure — they are signals. Learn everything from basic try/catch to advanced real-world error architectures used in production apps.

    What You'll Learn in This Lesson

    • Catch errors with try/catch/finally
    • Throw your own custom errors
    • Identify built-in error types (TypeError, ReferenceError)
    • Handle errors in async code
    • Create custom error classes
    • Build robust production-level error systems

    💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

    • Download Node.js to run JavaScript on your computer
    • Use your browser's Developer Console (Press F12) to test code snippets
    • Create a .html file with <script> tags and open it in your browser

    Why Error Handling Matters

    When errors are ignored:

    • Your app crashes suddenly
    • Users lose progress
    • Data becomes corrupted
    • Security becomes weaker
    • Debugging becomes 10× harder

    JavaScript gives you extremely powerful tools to handle, detect, filter, categorize, and recover from errors — synchronously AND asynchronously.

    1. What Is an Error in JavaScript?

    An error occurs when JavaScript cannot complete an operation. Examples:

    • Trying to use an undefined variable
    • Calling a function that doesn't exist
    • Invalid API responses
    • Network failures
    • Logic errors in your code
    • Unexpected data formats

    By default, errors stop execution. Your job is to catch, interpret, and recover from them.

    2. Try...Catch — The Foundation

    try {
        // Code that might fail
        riskyOperation();
    } catch (error) {
        console.error("Error occurred:", error.message);
    } finally {
        console.log("Cleanup runs no matter what.");
    }

    What happens:

    • try runs your potentially-dangerous code
    • catch receives the error if thrown
    • finally ALWAYS runs (cleanup, closing files, removing loaders, etc.)

    3. Throwing Your Own Errors

    JavaScript allows you to create and throw your own errors:

    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("Caught:", error.message);
    }

    Why throw custom errors?

    • You can prevent further execution
    • You can communicate clearly what went wrong
    • You can enforce data validation
    • You create predictable behaviour

    4. Built-In Error Types

    Error

    General-purpose error

    TypeError

    Incorrect data type. Example:

    null.toUpperCase();

    ReferenceError

    Variable not defined:

    console.log(x); // x is not declared

    SyntaxError

    Incorrect syntax:

    let a = ; // invalid

    RangeError

    Value outside allowed range:

    new Array(-2);

    URIError

    Triggered by malformed URI sequences

    5. Using Error Names to Handle Smartly

    try {
        risky();
    } catch (error) {
        if (error instanceof TypeError) {
            console.log("Type problem!");
        } else if (error instanceof ReferenceError) {
            console.log("You referenced something wrong!");
        } else {
            console.log("Unknown error:", error.message);
        }
    }

    This lets you create fine-grained error logic.

    6. Error Handling in Asynchronous Code

    With async/await:

    async function getUser() {
        try {
            const res = await fetch(url);
            if (!res.ok) throw new Error("HTTP Error " + res.status);
    
            return await res.json();
        } catch (err) {
            console.error("Fetch failed:", err.message);
            return { error: true, msg: err.message };
        }
    }

    Why async error handling matters:

    • Real-world JavaScript is 70% async
    • APIs fail
    • Network issues happen
    • Users have slow internet
    • Data can be malformed

    7. Re-Throwing Errors

    Sometimes you want to catch an error, log it, then pass it upward:

    async function loadData() {
        try {
            return await getData();
        } catch (err) {
            console.error("Handled locally:", err.message);
            throw err; // re-throw
        }
    }

    Useful when:

    • Logging errors
    • Adding context
    • Creating layered architecture

    8. Creating Custom Error Classes

    class ValidationError extends Error {
        constructor(message) {
            super(message);
            this.name = "ValidationError";
        }
    }
    
    function validateAge(age) {
        if (age < 0) throw new ValidationError("Age cannot be negative");
    }

    Benefits of custom errors:

    • Cleaner error organization
    • More predictable logic
    • Easier debugging
    • Perfect for large apps

    9. Nested Try...Catch Blocks

    try {
        try {
            throw new Error("Inner failure");
        } catch (inner) {
            console.log("Inner caught:", inner.message);
            throw new Error("Outer failure");
        }
    } catch (outer) {
        console.log("Outer caught:", outer.message);
    }

    Nested errors often appear when:

    • Parsing inside processing
    • Handling multiple API calls
    • Running multi-stage operations

    10. Best Practices for Error Handling

    Use specific error messages: "Invalid input — expected number, got string" is better than "Error!"
    Validate user input early
    Separate error logging & error display: Log detailed error on backend, show user-friendly message on frontend
    Don't leak sensitive info: Never show database errors or stack traces to users
    Always handle async errors: Most real failures happen in async code
    Use custom errors for clarity
    Fail safely (not silently)
    Use finally for cleanup: Closing connections, removing loaders

    11. Real-World Example — Payment Processing

    function chargeCard(card, amount) {
        if (!card.number) {
            throw new ValidationError("Card number missing");
        }
        if (amount <= 0) {
            throw new ValidationError("Amount must be greater than 0");
        }
    
        // Simulate charge...
        return { success: true };
    }
    
    // With layered handling:
    try {
        chargeCard(card, 10);
    } catch (err) {
        if (err instanceof ValidationError) {
            console.log("User made a mistake:", err.message);
        } else {
            console.log("Internal failure:", err.message);
        }
    }

    This mimics real e-commerce systems.

    12. Real-World Example — Robust API Fetching

    async function fetchProduct(id) {
        try {
            const res = await fetch(`/product/${id}`);
    
            if (!res.ok) {
                if (res.status === 404) throw new Error("Product not found");
                if (res.status === 500) throw new Error("Server error");
            }
    
            const data = await res.json();
            return data;
    
        } catch (err) {
            console.error("API error:", err.message);
            return { error: true };
        }
    }

    Now your UI doesn't crash when the API fails — it gracefully recovers.

    13. Real-World Example — Form Validation

    function validateForm(user) {
        if (!user.email.includes("@")) {
            throw new ValidationError("Invalid email format");
        }
        if (user.password.length < 8) {
            throw new ValidationError("Password too short");
        }
        return true;
    }
    
    // Frontend:
    try {
        validateForm(user);
        console.log("Form valid");
    } catch (err) {
        showErrorToUser(err.message);
    }

    14. Advanced Async Error Handling

    Asynchronous code is where 90% of real production errors happen.

    Why?

    • APIs timeout
    • Backend services go down
    • Internet drops
    • JSON parsing fails
    • Unexpected response shape
    • Race conditions
    • Rate limits
    • Authentication tokens expire

    You MUST master async error strategies to build real-world websites, apps, and SaaS systems.

    15. Fetch Errors Aren't Always "Errors"

    fetch() only throws errors for:

    • Network down
    • Domain unreachable
    • DNS failure
    • CORS failures
    • Request blocked

    ⚠️ It does NOT throw errors for bad HTTP codes (404, 500, 403, 429)

    You must check:

    if (!res.ok) {
        throw new Error("HTTP " + res.status);
    }

    Without this check, your code will behave like everything is fine when it absolutely isn't.

    16. Retry Logic with Exponential Backoff

    Real-world apps must recover from errors gracefully:

    async function retryFetch(url, retries = 3) {
        for (let i = 0; i < retries; i++) {
            try {
                const res = await fetch(url);
                if (res.ok) return res.json();
    
            } catch (err) {
                if (i === retries - 1) throw err;
            }
    
            await new Promise(r => setTimeout(r, 200 * (i + 1)));
        }
    }

    Retries at: 200ms → 400ms → 600ms. This is used by Google, AWS, Netflix, Stripe, PayPal.

    17. Defensive Programming

    Defensive programming = writing code that assumes things will go wrong.

    Defensive JSON Parsing:

    function safeJSON(response) {
        try {
            return response.json();
        } catch {
            return { error: "Invalid JSON" };
        }
    }

    Defensive Type Checking:

    function safeMultiply(a, b) {
        if (typeof a !== "number" || typeof b !== "number") {
            throw new TypeError("Arguments must be numbers");
        }
        return a * b;
    }

    Defensive Optional Chaining:

    const city = user?.address?.location?.city ?? "Unknown";

    No more crashes.

    18. Global Error Handling

    Most frameworks implement a global error handler:

    Browser Global Error Handler:

    window.onerror = function(message, source, line, column, error) {
        console.log("Global error:", message);
    };

    Global Promise Rejections:

    window.onunhandledrejection = function(event) {
        console.log("Unhandled rejection:", event.reason);
    };

    This prevents silent failures.

    19. Production-Grade Error Handler

    A complete production-grade Fetch handler:

    async function api(url, options = {}) {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), 8000);
    
        try {
            const res = await fetch(url, {
                ...options,
                signal: controller.signal
            });
    
            if (!res.ok) {
                const text = await res.text();
                throw new Error(`HTTP ${res.status}: ${text}`);
            }
    
            return res.json();
        } catch (e) {
            if (e.name === "AbortError") {
                throw new Error("Request timed out");
            }
            throw e;
        } finally {
            clearTimeout(timeout);
        }
    }

    This handles:

    • Timeout
    • Cancellation
    • Status errors
    • Network errors
    • Clean-up
    • JSON parsing

    Comprehensive Error Handling Examples

    Practice all error handling concepts including try/catch, custom errors, async handling, and defensive programming

    Try it Yourself »
    JavaScript
    // Comprehensive Error Handling Examples
    console.log("=== Error Handling Mastery ===\n");
    
    // 1. Basic try...catch
    console.log("1. Basic Try...Catch:");
    try {
        console.log("Trying risky operation...");
        throw new Error("Something went wrong!");
    } catch (error) {
        console.error("Caught error:", error.message);
    } finally {
        console.log("Cleanup complete\n");
    }
    
    // 2. Custom error class
    console.log("2. Custom Error Class:");
    class ValidationError extends Error {
        constructor(message)
    ...

    20. Common JavaScript Error Scenarios

    • ✔ JSON parsing failure
    • ✔ Network offline
    • ✔ Missing properties
    • ✔ Undefined variable
    • ✔ Null access
    • ✔ Wrong type
    • ✔ Not catching async error
    • ✔ Silent Promise rejection
    • ✔ Invalid function argument
    • ✔ Race condition

    You will encounter these daily in real development.

    🎯 Practice Projects for Mastery

    Project 1 — "Safe Calculator"

    • Validate numbers
    • Throw errors for wrong input
    • Catch and display friendly messages
    • Try/catch around each operation

    Project 2 — "Robust Fetch Wrapper"

    Build a function: safeFetch(url, retries = 3)

    • Retry logic
    • Throw on bad HTTP codes
    • Distinguish network vs HTTP error
    • Return JSON or fallback

    Project 3 — "Form Validator"

    • Use multiple custom error classes
    • Validate email, password, username
    • Show UI messages

    Project 4 — "Global Error Handler Demo"

    • Demonstrate window.onerror
    • Demonstrate window.onunhandledrejection

    📋 Quick Reference — Error Handling

    MethodPurpose
    try...catchCatch errors in a block of code
    throwManually trigger an error
    finallyRun code regardless of outcome
    Error ObjectHas .message and .stack
    Custom Errorclass MyError extends Error

    Lesson 10 Complete — Error Handling!

    You've mastered the art of writing robust code that handles failure gracefully. This separates amateur code from professional software.

    Up next: Closures & Scope — diving deep into advanced JavaScript concepts! 🧠

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service