IntermediateTypeScript · Lesson 5 of 9

Error Handling

try/catch with unknown errors, custom error classes and the Result pattern.

In TypeScript the value caught by catch is unknown, because anything can be thrown. Narrow it with instanceof Error before reading .message.

Custom error classes make failures meaningful: callers can check err instanceof NotFoundError and respond correctly (for example, return HTTP 404 instead of 500). Attach the original error with { cause } so you don't lose the root cause.

For expected failures — invalid input, not found — returning a Result object instead of throwing makes the failure part of the function's type, so callers can't forget to handle it. Reserve exceptions for truly unexpected situations.

errors.tsTypeScript
class NotFoundError extends Error {
  constructor(resource: string, id: string) {
    super(resource + " " + id + " not found");
    this.name = "NotFoundError";
  }
}

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

function parseAge(input: string): Result<number> {
  const n = Number(input);
  if (!Number.isInteger(n)) return { ok: false, error: "Age must be a whole number" };
  if (n < 0 || n > 120) return { ok: false, error: "Age must be between 0 and 120" };
  return { ok: true, value: n };
}

const db = new Map([["s1", "Amina"]]);

function getStudent(id: string): string {
  const name = db.get(id);
  if (!name) throw new NotFoundError("Student", id);
  return name;
}

function loadConfig(text: string): unknown {
  try {
    return JSON.parse(text);
  } catch (err) {
    throw new Error("Config is not valid JSON", { cause: err });
  }
}

for (const input of ["17", "abc", "200"]) {
  const r = parseAge(input);
  console.log(input, r.ok ? "-> " + r.value : "-> error: " + r.error);
}

try {
  getStudent("s9");
} catch (err) {
  if (err instanceof NotFoundError) console.log("404:", err.message);
  else throw err;
}

try {
  loadConfig("{bad json");
} catch (err) {
  if (err instanceof Error) {
    const cause = err.cause instanceof Error ? err.cause.message : String(err.cause);
    console.log(err.message, "| cause:", cause);
  }
}

Key points

  • Caught values are unknown — check instanceof Error first.
  • Custom error classes let callers handle each failure differently.
  • Return a Result for expected failures; throw for unexpected ones. Never swallow errors silently.

Exercise

Write parseScoreLine(line) that turns "Amina,88" into { name, score } and returns a Result with clear error messages for missing commas, empty names and invalid scores. Process a list of lines and print the valid rows and the error count.

Show solution

Try the exercise yourself first — then compare your approach with this one.

parseScoreLine returns a Result instead of throwing, so each failure is an ordinary value with a clear message. The caller separates valid rows from errors without any try/catch.

parse-scores.tsTypeScript
type Result<T, E = string> = { ok: true; value: T } | { ok: false; error: E };

interface ScoreRow {
  name: string;
  score: number;
}

function parseScoreLine(line: string): Result<ScoreRow> {
  const parts = line.split(",");
  if (parts.length !== 2) return { ok: false, error: "Expected 'name,score' but got '" + line + "'" };

  const name = parts[0].trim();
  if (name === "") return { ok: false, error: "Name is empty in '" + line + "'" };

  const score = Number(parts[1]);
  if (!Number.isInteger(score) || score < 0 || score > 100) {
    return { ok: false, error: "Invalid score in '" + line + "'" };
  }
  return { ok: true, value: { name, score } };
}

const lines = ["Amina,88", "Juma 42", ",70", "Neema,abc", "Ali,95"];
const valid: ScoreRow[] = [];
let errors = 0;

for (const line of lines) {
  const r = parseScoreLine(line);
  if (r.ok) valid.push(r.value);
  else {
    errors++;
    console.log("Skipped:", r.error);
  }
}

console.log(valid);                      // [ { name: 'Amina', score: 88 }, { name: 'Ali', score: 95 } ]
console.log("Errors:", errors);          // Errors: 3

Check your understanding

  1. What is the type of err in catch (err) with strict TypeScript settings?

  2. When is returning a Result object better than throwing?

  3. What does new Error("Config is not valid JSON", { cause: err }) preserve?

  4. Which is a bad practice?

Ask AI