JavaScript Error Handling

Production JavaScript never crashes silently. Learn try/catch/finally, the Error object hierarchy, how to throw meaningful errors, and how to handle failures in async code.

Beginner 10 min read 10 examples

try / catch

Wrap code that might throw an error in a try block. If any line throws, execution jumps immediately to the catch block with the error object.

JavaScript
// Basic try/catch
try {
    const result = JSON.parse("invalid json{");
    console.log(result); // never reached
} catch (error) {
    console.log(error.name);    // "SyntaxError"
    console.log(error.message); // "Unexpected token i in JSON..."
}

// Code after catch continues normally
console.log("Program continues"); // this runs

// Accessing a property on null
try {
    const user = null;
    console.log(user.name); // TypeError!
} catch (err) {
    console.log("Caught:", err.message);
    // "Caught: Cannot read properties of null"
}

// Nested try/catch
try {
    try {
        throw new Error("Inner error");
    } catch (inner) {
        console.log("Inner catch:", inner.message);
        throw inner; // re-throw to outer catch
    }
} catch (outer) {
    console.log("Outer catch:", outer.message);
}

The Error Object

When an error is caught, the catch parameter is an Error object with useful properties:

JavaScript
try {
    null.property; // throws TypeError
} catch (err) {
    console.log(err.name);    // "TypeError"
    console.log(err.message); // "Cannot read properties of null"
    console.log(err.stack);   // Full stack trace (for debugging)
    console.log(err instanceof TypeError); // true
    console.log(err instanceof Error);     // true (all errors extend Error)
}

// Creating errors manually
const e = new Error("Something went wrong");
console.log(e.name);    // "Error"
console.log(e.message); // "Something went wrong"

// Logging error objects
function logError(err) {
    console.error(`[${err.name}] ${err.message}`);
    if (process.env.NODE_ENV === "development") {
        console.error(err.stack);
    }
}

Built-in Error Types

Error TypeWhen it occursExample
ErrorGeneric base errorthrow new Error("msg")
TypeErrorWrong type usednull.property
ReferenceErrorUndefined variable accessedundeclaredVar
SyntaxErrorInvalid code syntaxJSON.parse("{")
RangeErrorValue out of valid rangenew Array(-1)
URIErrorBad URI encodingdecodeURI("%")
JavaScript
// Handling different error types differently
try {
    riskyOperation();
} catch (err) {
    if (err instanceof TypeError) {
        console.log("Type problem:", err.message);
    } else if (err instanceof RangeError) {
        console.log("Range problem:", err.message);
    } else {
        throw err; // re-throw unexpected errors
    }
}

Throwing Errors

Use throw to raise an error intentionally - for invalid input, business rule violations, or unexpected states. Always throw Error objects (not strings or numbers).

JavaScript
// Throw an Error object (always use Error, not raw strings)
function divide(a, b) {
    if (typeof a !== "number" || typeof b !== "number") {
        throw new TypeError("Arguments must be numbers");
    }
    if (b === 0) {
        throw new RangeError("Cannot divide by zero");
    }
    return a / b;
}

try {
    console.log(divide(10, 2));   // 5
    console.log(divide(10, 0));   // throws RangeError
} catch (err) {
    console.log(`${err.name}: ${err.message}`);
    // "RangeError: Cannot divide by zero"
}

// Throw stops execution immediately
function validate(user) {
    if (!user.name) throw new Error("Name is required");
    if (!user.email) throw new Error("Email is required");
    if (user.age < 0) throw new RangeError("Age cannot be negative");
    return true;
}

// Anti-pattern: throwing strings (loses stack trace)
// throw "something went wrong"; // avoid!

finally

The finally block always runs after try and catch, regardless of success or failure. Use it for cleanup.

JavaScript
// finally always runs
function fetchData(url) {
    let loading = true;
    try {
        const data = getData(url);
        return data;
    } catch (err) {
        console.error("Failed:", err.message);
        return null;
    } finally {
        loading = false;  // always hides spinner
        console.log("Request complete"); // always runs
    }
}

// finally with return - the finally return wins!
function example() {
    try {
        return "try";
    } finally {
        return "finally"; // overrides try's return
    }
}
console.log(example()); // "finally"

// Practical: close database connection
async function runQuery(sql) {
    const conn = await db.connect();
    try {
        return await conn.query(sql);
    } catch (err) {
        throw err; // re-throw after logging
    } finally {
        await conn.close(); // always close the connection
    }
}

Custom Error Classes

Extend the built-in Error class to create domain-specific errors with additional context. This makes instanceof checks possible.

JavaScript
// Custom error class
class ValidationError extends Error {
    constructor(message, field) {
        super(message);
        this.name = "ValidationError";
        this.field = field;
    }
}

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

// Using custom errors
function getUser(id) {
    const user = db.findById(id);
    if (!user) throw new NotFoundError("User", id);
    return user;
}

function createUser(data) {
    if (!data.email) throw new ValidationError("Email is required", "email");
    if (!data.name)  throw new ValidationError("Name is required", "name");
    return db.insert(data);
}

// Handling custom errors
try {
    createUser({ name: "Alice" }); // missing email
} catch (err) {
    if (err instanceof ValidationError) {
        console.log(`Validation failed on field '${err.field}': ${err.message}`);
    } else if (err instanceof NotFoundError) {
        console.log(`Not found (${err.statusCode}): ${err.message}`);
    } else {
        throw err; // re-throw unexpected errors
    }
}

Async Error Handling

JavaScript
// try/catch with async/await
async function loadUser(id) {
    try {
        const response = await fetch(`/api/users/${id}`);
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        return await response.json();
    } catch (err) {
        console.error("Failed to load user:", err.message);
        return null;
    }
}

// Promise .catch()
fetch("/api/data")
    .then(res => res.json())
    .then(data => console.log(data))
    .catch(err => console.error("Error:", err.message));

// Global unhandled rejection handler (browser)
window.addEventListener("unhandledrejection", event => {
    console.error("Unhandled promise rejection:", event.reason);
    event.preventDefault(); // prevents browser default logging
});

// Node.js unhandled rejections
process.on("unhandledRejection", (reason, promise) => {
    console.error("Unhandled Rejection at:", promise, "reason:", reason);
    process.exit(1);
});

Frequently Asked Questions

No. Only catch errors at boundaries where you can meaningfully handle them - at the top of an operation, in event handlers, or in API calls. Wrapping every function in try/catch adds noise and hides bugs. Let errors propagate naturally until they reach a layer that can display a useful message, retry the operation, or fall back to a default state.

JavaScript has 7 built-in error types: Error (base), TypeError (wrong type used), ReferenceError (undefined variable), SyntaxError (invalid code - caught at parse time), RangeError (value out of valid range), URIError (bad URI encoding), and EvalError (deprecated). Check the type with instanceof to handle each differently in a catch block.

Wrap await calls in a try/catch block. A rejected promise inside await throws at that line. Alternatively, you can chain .catch() on a promise before awaiting: const data = await fetch(url).catch(err => null). For consistent async error handling across an app, use a global unhandled promise rejection handler: window.addEventListener("unhandledrejection", handler).

The finally block always runs regardless of whether an error was thrown or caught. It is used for cleanup operations that must happen either way: closing database connections, hiding loading spinners, releasing locks, or clearing timers. It runs even if the try or catch block has a return statement - the finally code executes before the function actually returns.