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
Exercise: TypeScript Error Handling
With useUnknownInCatchVariables (on by default under strict), what type is a caught error given in `catch (err)`?