TypeScript Error Handling

TypeScript treats caught errors as unknown by default, pushing you toward safe narrowing and well-typed custom error classes instead of guesswork.

Why unknown in catch Blocks

JavaScript lets you `throw` any value at all, not just an `Error`. Because of that, TypeScript types the variable in a `catch` clause as `unknown` (under the `useUnknownInCatchVariables` option, on by default in strict mode), which forces you to narrow it with `instanceof` or a type guard before touching any of its properties.

Narrowing an Unknown Error

function parseConfig(json: string): Record<string, unknown> {
  return JSON.parse(json);
}

try {
  parseConfig("{ invalid json");
} catch (error: unknown) {
  if (error instanceof SyntaxError) {
    console.error("Bad JSON:", error.message);
  } else if (error instanceof Error) {
    console.error("Unexpected error:", error.message);
  } else {
    console.error("Unknown failure:", error);
  }
}

Custom Error Classes

Extending the built-in `Error` class lets you attach extra, strongly typed context - like which field failed validation - while still behaving like a normal error for logging, stack traces, and `instanceof` checks. Always set `this.name` so logs clearly show which error type was thrown.

A Custom Error Class

class ValidationError extends Error {
  constructor(message: string, public field: string) {
    super(message);
    this.name = "ValidationError";
    Object.setPrototypeOf(this, ValidationError.prototype);
  }
}

function validateAge(age: number): void {
  if (age < 0) {
    throw new ValidationError("Age cannot be negative", "age");
  }
}

try {
  validateAge(-5);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(`${error.name}: ${error.message} (field: ${error.field})`);
  }
}

Typed Result Patterns

For failures you expect to happen regularly - like a form validation error or a divide-by-zero - returning a typed `Result<T, E>` object can be clearer than throwing, because the caller is forced by the type system to check `ok` before touching `value`, rather than relying on remembering to wrap a call in `try`/`catch`.

A Result Type Instead of Throwing

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return { ok: false, error: "Division by zero" };
  }
  return { ok: true, value: a / b };
}

const outcome = divide(10, 0);
if (outcome.ok) {
  console.log("Result:", outcome.value);
} else {
  console.error("Error:", outcome.error);
}
  • Extend Error rather than throwing plain objects or strings
  • Always set this.name in a custom error's constructor
  • Narrow caught errors with instanceof, not string matching on messages
  • Prefer Result<T, E> for expected, recoverable failures
  • Reserve throw/catch for truly exceptional, unrecoverable situations
StrategyBest forTradeoff
throw/catch with custom classesUnexpected or exceptional failuresCaller must remember to catch
Result<T, E> return typeExpected, recoverable failuresMore verbose call sites, explicit checks
Promise rejectionAsync failuresSame narrowing rules apply inside catch
Note: TypeScript 4.4 and later types catch variables as unknown by default whenever strict mode is on, via useUnknownInCatchVariables - if you're on an older codebase still seeing catch variables typed as any, that flag is worth turning on separately.
Note: When compiling custom Error subclasses down to targets older than ES2015, instanceof checks can silently fail because of how JavaScript engines handle built-in subclassing - call Object.setPrototypeOf(this, YourError.prototype) in the constructor as a safety net.

Exercise: TypeScript Error Handling

With useUnknownInCatchVariables (on by default under strict), what type is a caught error given in `catch (err)`?