Skip to main content

Deliberate Error Handling

intermediate14 min readLesson 62 of 143

Expected vs unexpected failures, custom error classes, targeted try/catch, and the result-object pattern โ€” an error strategy for real programs.

You already know an uncaught error crashes a program. What separates intermediate code from beginner code is a deliberate error strategy: which errors you expect, which you let crash, and how a failure travels between the layers of a program.

Expected vs unexpected errors

An expected error is part of the problem domain: a form field is empty, a JSON string is malformed, a record does not exist. An unexpected error is a bug: a typo, a missing function, an impossible state. You handle expected errors and you fix unexpected ones. Wrapping your whole program in try/catch hides bugs โ€” the worst outcome.

The shape of an Error

throw accepts any value, but always throw Error instances โ€” they carry a message, a stack (dev-only detail), and since ES2022 a cause for chaining:

try {
  loadConfig(path);
} catch (err) {
  throw new Error("Config failed to load", { cause: err });
}

The built-in types have meaning: TypeError for wrong types, RangeError for out-of-range values, SyntaxError from JSON.parse. Reach for them before inventing your own.

Custom errors

When callers need to react differently to a failure, give it a name:

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

throw new ValidationError("age must be non-negative", "age");

Callers can now distinguish it with err instanceof ValidationError โ€” string-matching messages is how bugs hide.

Catch deliberately, narrowly

Catch only what you can actually handle, and rethrow the rest:

try {
  const user = parseUser(raw);
  save(user);
} catch (err) {
  if (err instanceof ValidationError) {
    showFieldError(err.field, err.message); // ours: show a message
  } else {
    throw err; // not ours: let it propagate
  }
}

The optional finally block runs whether or not an error occurred โ€” use it to release resources, not to return values.

The result-object pattern

Across module boundaries, expected failures are often better as values than thrown exceptions. Return { ok: true, value } or { ok: false, error } and force the caller to acknowledge the failure โ€” this is how the parser practice below is shaped. Rule of thumb: throw for programmer-visible failures, return results for ordinary, expected ones.

Now practice

Error Handling โ€” PracticeCatch safely, define custom errors, and retry failures: a safe JSON parser, a ValidationError class, and a retry helper.3 challenges ยท ยท ~18 min