TypeScript Error Handling
Error handling is the practice of writing code that anticipates problems and responds to them gracefully, instead of crashing. TypeScript strengthens JavaScript's existing error-handling tools — try, catch, finally, and custom error classes — with type safety, making error management predictable and structured.
Basic try / catch / finally
CODE FLOW:
try block runs
│
├── Error thrown? → catch block runs
│
└── No error? → catch block skipped
│
(either path) → finally block always runs
function divideNumbers(a: number, b: number): number {
try {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
} catch (error) {
console.log("Caught an error:", error);
return 0;
} finally {
console.log("divide() finished"); // runs no matter what
}
}
console.log(divideNumbers(10, 2)); // 5 — divide() finished
console.log(divideNumbers(10, 0)); // Caught an error: Cannot divide by zero
// 0 — divide() finished
The Type of error in catch
In TypeScript (with useUnknownInCatchVariables: true or strict mode), the error variable in a catch block is typed as unknown. This forces developers to check what kind of error was thrown before accessing its properties.
try {
throw new Error("Something failed");
} catch (error: unknown) {
// error is unknown — must check before accessing .message
if (error instanceof Error) {
console.log(error.message); // "Something failed"
console.log(error.name); // "Error"
} else {
console.log("Unknown error:", error);
}
}
Custom Error Classes
Custom error classes extend the built-in Error class to create specific error types. Code that catches errors can then identify exactly what went wrong using instanceof.
STANDARD ERROR TYPES — what you can build:
Error (base)
├── ValidationError → form/input problems
├── NetworkError → API/connection problems
├── AuthError → login/permission problems
└── NotFoundError → missing resource problems
// Define custom errors
class AppError extends Error {
constructor(
message: string,
public readonly code: string
) {
super(message);
this.name = "AppError";
}
}
class ValidationError extends AppError {
constructor(
message: string,
public readonly field: string
) {
super(message, "VALIDATION_ERROR");
this.name = "ValidationError";
}
}
class NetworkError extends AppError {
constructor(
message: string,
public readonly statusCode: number
) {
super(message, "NETWORK_ERROR");
this.name = "NetworkError";
}
}
class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, "NOT_FOUND");
this.name = "NotFoundError";
}
}
Using Custom Errors
function validateAge(age: number): void {
if (age < 0 || age > 120) {
throw new ValidationError(
`Age ${age} is not valid`,
"age"
);
}
}
function fetchUserById(id: number): { name: string } {
if (id !== 1) {
throw new NotFoundError(`User with ID ${id}`);
}
return { name: "Neha" };
}
function processUser(id: number, age: number): void {
try {
validateAge(age);
let user = fetchUserById(id);
console.log(`Processing ${user.name}, age ${age}`);
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation failed on field '${error.field}': ${error.message}`);
} else if (error instanceof NotFoundError) {
console.log(`Not found: ${error.message}`);
} else if (error instanceof AppError) {
console.log(`App error [${error.code}]: ${error.message}`);
} else {
console.log("Unexpected error:", error);
}
}
}
processUser(1, 25); // Processing Neha, age 25
processUser(99, 25); // Not found: User with ID 99 not found
processUser(1, -5); // Validation failed on field 'age': Age -5 is not valid
Result Pattern — Errors Without Throwing
Some teams prefer returning errors as values instead of throwing them. The Result pattern wraps a function's output in an object that is either a success or a failure.
type Result<T> =
| { success: true; value: T }
| { success: false; error: string };
function parseNumber(input: string): Result<number> {
let num = Number(input);
if (isNaN(num)) {
return { success: false, error: `"${input}" is not a valid number` };
}
return { success: true, value: num };
}
let r1 = parseNumber("42");
let r2 = parseNumber("abc");
if (r1.success) {
console.log("Parsed:", r1.value); // Parsed: 42
} else {
console.log("Error:", r1.error);
}
if (r2.success) {
console.log("Parsed:", r2.value);
} else {
console.log("Error:", r2.error); // Error: "abc" is not a valid number
}
Error Handling with Async Code
// Using try/catch with async/await
async function loadUserData(userId: number): Promise<void> {
try {
let response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new NetworkError(
`Failed to load user ${userId}`,
response.status
);
}
let data = await response.json();
console.log("User:", data);
} catch (error) {
if (error instanceof NetworkError) {
console.log(`Network error ${error.statusCode}: ${error.message}`);
} else if (error instanceof Error) {
console.log(`Error: ${error.message}`);
}
}
}
Practical: Form Validation Error System
interface FormData {
username: string;
email: string;
age: number;
}
class FormValidationError extends Error {
public errors: Record<string, string> = {};
constructor(errors: Record<string, string>) {
super("Form validation failed");
this.name = "FormValidationError";
this.errors = errors;
}
}
function validateForm(data: FormData): void {
let errors: Record<string, string> = {};
if (data.username.length < 3) {
errors["username"] = "Username must be at least 3 characters";
}
if (!data.email.includes("@")) {
errors["email"] = "Email must contain @";
}
if (data.age < 18) {
errors["age"] = "Must be 18 or older";
}
if (Object.keys(errors).length > 0) {
throw new FormValidationError(errors);
}
}
try {
validateForm({ username: "Jo", email: "notanemail", age: 15 });
} catch (error) {
if (error instanceof FormValidationError) {
console.log("Validation errors:");
for (let [field, msg] of Object.entries(error.errors)) {
console.log(` ${field}: ${msg}`);
}
}
}
// Validation errors:
// username: Username must be at least 3 characters
// email: Email must contain @
// age: Must be 18 or older
Error Handling Best Practices
| Practice | Reason |
|---|---|
Use instanceof in catch | Identifies exact error type safely |
| Build custom error classes | Makes errors descriptive and catchable by type |
| Never silence errors with empty catch | Hides bugs; always log or rethrow |
Use finally for cleanup | Closes connections, releases resources |
| Use Result pattern for expected failures | Avoids exceptions for predictable cases |
Type the catch variable as unknown | Forces proper checking before use |
