19 / 20 · Day 7
Day 7 · Concept 19

Error handling

try / catch / finally for synchronous flows; .catch() or try / await for promises. Always throw Error (or a subclass), never strings. Custom error classes carry semantic meaning and unlock matching.


1 · try / catch / finally

js try.js
function parseAge(s) {
    const n = Number(s);
    if (Number.isNaN(n)) throw new TypeError("not a number: " + s);
    if (n < 0)            throw new RangeError("negative age: " + n);
    return n;
}

try {
    console.log(parseAge("twenty"));
} catch (err) {
    console.log(err.name, "-", err.message);
} finally {
    console.log("cleanup runs either way");
}

finally runs whether the try succeeds, throws, or returns early. Use it for resource cleanup — closing files, releasing locks, restoring state.

2 · The Error type and its subclasses

Built-inFor
ErrorGeneric. Use when none of the others fit.
TypeErrorValue of the wrong type — null where object expected, non-function called.
RangeErrorNumeric out of range — stack overflow, invalid array length.
SyntaxErrorParse error. Usually JSON.parse or eval.
ReferenceErrorVariable not declared.
AggregateErrorMultiple errors at once — from Promise.any.

Each carries name, message, and a stack trace. The type alone tells the caller something.

3 · Custom error classes

js custom.js
class ValidationError extends Error {
    constructor(field, reason) {
        super(`validation failed: ${field} (${reason})`);
        this.name = "ValidationError";
        this.field = field;
        this.reason = reason;
    }
}

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

function fetchUser(id) {
    if (!id)        throw new ValidationError("id", "required");
    if (id < 0)     throw new ValidationError("id", "must be positive");
    if (id > 1000)  throw new NotFoundError("user/" + id);
    return { id, name: "Alice" };
}

try {
    fetchUser(9999);
} catch (err) {
    if (err instanceof NotFoundError) {
        console.log("404:", err.resource);
    } else if (err instanceof ValidationError) {
        console.log("400:", err.field, err.reason);
    } else {
        throw err;
    }
}
Why subclasses. Callers can instanceof-match and handle each class differently. Stringly-typed errors lose this; checking err.message against a substring is fragile and reads badly in code review.

4 · Errors in async code

js async-errors.js
async function load() {
    throw new Error("boom");
}

// With await — try/catch works
async function main() {
    try {
        await load();
    } catch (err) {
        console.log("caught via await:", err.message);
    }
}

// With .then — use .catch
load().catch(err => console.log("caught via .catch:", err.message));

main();

A rejected promise is an error path. Always handle it — either with .catch or a surrounding try / await. Unhandled rejections are eventual process-killers in Node and "Uncaught (in promise)" warnings in the browser.

5 · The cause property (ES2022)

js cause.js
async function load(id) {
    try {
        await fetch("/api/" + id);  // fictional — pretend this throws
    } catch (err) {
        throw new Error("failed to load " + id, { cause: err });
    }
}

(async () => {
    try {
        await load(42);
    } catch (err) {
        console.log("outer:", err.message);
        console.log("inner:", err.cause?.message);
    }
})();

new Error(msg, { cause }) chains the original error. Preserves stack context across re-throws without losing the root cause. Use it at layer boundaries.

6 · Patterns to use

  • Throw early, catch late. Validate at the entry point; let the rest of the function assume valid inputs. Catch where you can do something useful (log, retry, return fallback).
  • Catch specific, rethrow unknown. if (err instanceof MyError) handle(); else throw err; — never silently swallow.
  • Convert at boundaries. A library catches low-level errors and rethrows domain errors. DatabaseError wraps a connection refused.
  • finally for cleanup, not for return values. A return in finally overrides the try's return value — almost always a bug.

7 · Common mistakes

  • throw "bad" — strings have no stack trace, no name. Always throw Error.
  • Swallowing errors. catch (e) {} hides bugs. At least log; ideally re-throw or handle.
  • Missing .catch on a fire-and-forget promise. Use void load() or attach a .catch for logging.
  • Catching too wide. catch (e) grabs everything — including bugs you should let crash. Match specific types.
  • Throwing from a setTimeout / setInterval callback. Doesn't propagate to your try / catch. Wrap the callback body itself in try.

8 · When it clicks

  • You define custom error classes by reflex for new modules.
  • You reach for cause when wrapping a lower-level error.
  • You spot bare catch {} and unhandled promise chains in code review.
  • You stop trying to recover from TypeError / RangeError — those are bugs, not conditions.
Found this useful?