Academic Block

Error Handling (try/catch/finally) Get Certified

Errors are inevitable. Good programs do not crash when something goes wrong — they detect the problem, recover when possible, and leave useful information for debugging. JavaScript’s primary tool for this is the try...catch...finally statement.

Basic try…catch

Code that might fail goes inside the try block. If an error is thrown, control jumps to the catch block.

What this code does: Attempts to parse invalid JSON. The error is caught and a friendly message is logged instead of crashing.

console.log("Attempting to parse JSON...");

try {
    const data = JSON.parse("this is not valid JSON");
    console.log(data);
} catch (error) {
    console.log("Caught an error:", error.message);
}

console.log("Program continues normally");

Throwing Your Own Errors

You can raise an error yourself with the throw keyword. Always throw an actual Error instance (or a subclass) so you get a useful stack trace.

What this code does: Manually throws an Error when an invalid value is detected, then catches and reports it.

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

console.log("Trying division...");

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

console.log("Finished");

The finally Block

The finally block always runs after try and catch, whether an error occurred or not. It is ideal for cleanup work.

What this code does: Demonstrates that finally runs both on success and on failure.

function runWithCleanup(shouldFail) {
    try {
        console.log("Working...");
        if (shouldFail) {
            throw new Error("Something went wrong");
        }
        console.log("Success");
    } catch (error) {
        console.log("Caught:", error.message);
    } finally {
        console.log("Cleanup always runs");
    }
}

console.log("--- Success case ---");
runWithCleanup(false);

console.log("--- Failure case ---");
runWithCleanup(true);

Error Properties

Every Error object has three useful properties: name, message, and stack.

What this code does: Creates an Error and logs its name, message, and a shortened version of the stack.

try {
    throw new Error("Something broke");
} catch (error) {
    console.log("name:", error.name);
    console.log("message:", error.message);
    console.log("stack (first line):", error.stack.split("\\n")[0]);
}

Built-in Error Types

JavaScript provides several specialized error constructors.

What this code does: Demonstrates TypeError, RangeError, and ReferenceError.

// TypeError
try {
    null.foo();
} catch (e) {
    console.log("TypeError:", e.message);
}

// RangeError
try {
    new Array(-1);
} catch (e) {
    console.log("RangeError:", e.message);
}

// ReferenceError
try {
    console.log(undeclaredVariable);
} catch (e) {
    console.log("ReferenceError:", e.message);
}

Creating Custom Error Types

You can create your own error types by extending the built-in Error class. This is useful when you want to distinguish between different kinds of application-specific errors.

What this code does: Defines a custom ValidationError that extends Error, then throws it when an invalid age is supplied.

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

function setAge(age) {
    if (age < 0) {
        throw new ValidationError("Age cannot be negative");
    }
    return age;
}

console.log("Setting age...");

try {
    setAge(-1);
} catch (error) {
    console.log(error.name + ":", error.message);
}

Catching Specific Error Types

Inside a catch block, you can use instanceof to check which kind of error was thrown and respond differently for each one.

What this code does: Throws different error types and uses instanceof in the catch block to handle each case.

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

function setAge(age) {
    if (typeof age !== "number") {
        throw new TypeError("Age must be a number");
    }
    if (age < 0) {
        throw new ValidationError("Age cannot be negative");
    }
    return age;
}

console.log("Testing validation...");

try {
    setAge(-1);
} catch (error) {
    if (error instanceof ValidationError) {
        console.log("Validation problem:", error.message);
    } else if (error instanceof TypeError) {
        console.log("Type problem:", error.message);
    } else {
        console.log("Unexpected error:", error.message);
    }
}

Rethrowing Errors

Sometimes you want to handle part of an error (such as logging it) but still let it propagate further up. You can do this by throwing the error again inside the catch block.

What this code does: Catches an error at a lower level, logs it, then re-throws so an outer handler can also react.

function riskyOperation() {
    throw new Error("Low-level failure");
}

function process() {
    try {
        riskyOperation();
    } catch (error) {
        console.log("Logged at process():", error.message);
        throw error; // rethrow
    }
}

console.log("Starting...");

try {
    process();
} catch (error) {
    console.log("Caught at top level:", error.message);
}

Nested try...catch Blocks

A try...catch statement can be placed inside another one, allowing you to handle errors at different levels.

What this code does: Shows nested try...catch blocks. The inner catch handles the error so the outer try continues.

try {
    console.log("Outer try starts");

    try {
        throw new Error("Inner failure");
    } catch (innerError) {
        console.log("Handled inside:", innerError.message);
    }

    console.log("Outer try continues normally");
} catch (outerError) {
    console.log("Outer catch:", outerError.message);
}

Optional Catch Binding

If you do not need the error object itself, you can omit the parentheses and parameter after catch.

What this code does: Uses a catch clause without a binding when the error details are not needed.

try {
    JSON.parse("not valid json");
    console.log("This will not run");
} catch {
    console.log("Invalid JSON — using default value instead.");
}

Error Handling with async/await

try...catch also works with async/await. A rejected promise thrown by await is caught the same way as a synchronous error.

What this code does: Awaits a Promise that rejects. The rejection is caught by the surrounding try...catch.

function loadData() {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            reject(new Error("Failed to load data"));
        }, 80);
    });
}

async function run() {
    try {
        console.log("Loading...");
        const data = await loadData();
        console.log(data);
    } catch (error) {
        console.log("Caught:", error.message);
    } finally {
        console.log("Attempt finished");
    }
}

run();
console.log("run() called");

Common Built-in Error Types

Error Type When It Is Thrown
TypeError A value is used in a way its type does not allow
RangeError A value falls outside an allowed range
ReferenceError A reference is made to an undeclared variable
SyntaxError Code or data (like JSON) is malformed
Error The general-purpose base type for custom errors

Common Mistakes with Error Handling

  • Wrapping too much code in one try block, making it unclear which line actually failed.
  • Catching an error and doing nothing with it (an empty catch block), hiding real problems.
  • Throwing plain strings or objects instead of an Error instance, losing the stack property.
  • Forgetting that finally always runs, even after a return inside try or catch.
  • Not checking the specific error type when different errors need different handling.

What this code does: Contrasts the bad practice of throwing a plain string with the recommended practice of throwing a real Error instance.

// Prefer throwing an Error instance:

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

try {
    goodExample();
} catch (error) {
    console.log("Caught properly:", error.message);
}

Best Practices for Error Handling

  • Keep try blocks small and focused on the code that can actually fail.
  • Always throw Error instances (or subclasses), never plain strings or numbers.
  • Use custom error classes to distinguish between different failure types in larger applications.
  • Never leave a catch block empty; at minimum, log the error.
  • Use finally for cleanup that must run regardless of success or failure.
  • Only catch errors you can actually handle; let unexpected ones propagate or be logged centrally.

Quick Summary

  • try runs code that might fail; catch handles the error if one occurs.
  • finally always runs, whether an error occurred or not.
  • throw raises an error manually, usually as an Error instance.
  • Every error has a name, a message, and a stack.
  • Built-in error types include TypeError, RangeError, ReferenceError, and SyntaxError.
  • Custom error classes can extend Error for application-specific errors.
  • try...catch also works with async/await.

Try It Yourself

Modify the code below. Try changing the age to a negative number, passing a string instead of a number, or adding your own custom error class.

What this code does: A complete practice example that validates age, throws typed errors, distinguishes them with instanceof, and always runs the finally block.

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

function setAge(age) {
    if (typeof age !== "number") {
        throw new TypeError("Age must be a number");
    }
    if (age < 0) {
        throw new ValidationError("Age cannot be negative");
    }
    return age;
}

try {
    console.log("Setting age...");
    console.log(setAge(28));
} catch (error) {
    if (error instanceof ValidationError) {
        console.log("Validation problem:", error.message);
    } else if (error instanceof TypeError) {
        console.log("Type problem:", error.message);
    } else {
        console.log("Unexpected error:", error.message);
    }
} finally {
    console.log("Done setting age");
}
Tip: Keep try blocks small, always throw real Error objects, never leave a catch block empty, and use finally for cleanup that has to run no matter what happens.

Understanding try, catch, and finally ties together everything you have learned about promises, async/await, and the Fetch API, giving you the tools to write JavaScript that fails gracefully instead of crashing.

🧪 Test Your JavaScript Code

Edit the code on the left and click "Run Code" to see the console output on the right.

📝 JavaScript Code
👁 Console Output

Click "Run Code" to see the output here.

Ctrl + Enter to run code Esc to close