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

PracticeReason
Use instanceof in catchIdentifies exact error type safely
Build custom error classesMakes errors descriptive and catchable by type
Never silence errors with empty catchHides bugs; always log or rethrow
Use finally for cleanupCloses connections, releases resources
Use Result pattern for expected failuresAvoids exceptions for predictable cases
Type the catch variable as unknownForces proper checking before use

Leave a Comment

Your email address will not be published. Required fields are marked *