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.
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— checkinstanceof Errorfirst. - Custom error classes let callers handle each failure differently.
- Return a
Resultfor 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.
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