Lab 08: Error Handling

Objective

Handle errors with type safety: typed Error classes, Result/Either types, discriminated unions for errors, exhaustive handling, and never for impossible states.

Time

30 minutes

Prerequisites

  • Lab 02 (Functions), Lab 05 (Enums)

Tools

  • Docker image: zchencow/innozverse-ts:latest


Lab Instructions

Step 1: Typed Error Classes

// Custom error hierarchy
class AppError extends Error {
    constructor(
        message: string,
        public readonly code: string,
        public readonly context?: Record<string, unknown>,
    ) {
        super(message);
        this.name = this.constructor.name;
        // Fix prototype chain (needed for `instanceof`)
        Object.setPrototypeOf(this, new.target.prototype);
    }
}

class ValidationError extends AppError {
    constructor(
        public readonly field: string,
        message: string,
        public readonly value?: unknown,
    ) {
        super(message, "VALIDATION_ERROR", { field, value });
    }
}

class NotFoundError extends AppError {
    constructor(resource: string, id: number | string) {
        super(`${resource} #${id} not found`, "NOT_FOUND", { resource, id });
    }
}

class NetworkError extends AppError {
    constructor(url: string, public readonly statusCode: number) {
        super(`HTTP ${statusCode} from ${url}`, "NETWORK_ERROR", { url, statusCode });
    }
}

// Usage
function processAge(input: string): number {
    const age = parseInt(input);
    if (isNaN(age)) throw new ValidationError("age", `'${input}' is not a number`, input);
    if (age < 0 || age > 150) throw new ValidationError("age", `Age ${age} out of range`, age);
    return age;
}

["25", "abc", "-1", "200"].forEach(input => {
    try {
        console.log(`age=${processAge(input)}`);
    } catch (e) {
        if (e instanceof ValidationError) {
            console.log(`ValidationError[${e.field}]: ${e.message}`);
        } else {
            throw e;
        }
    }
});

💡 Object.setPrototypeOf(this, new.target.prototype) is essential when extending Error in TypeScript compiled to ES5. Without it, instanceof checks fail — new ValidationError() instanceof ValidationError returns false. With ES2015 target in tsconfig, this isn't needed.

📸 Verified Output:


Step 2: Result Type Pattern

💡 Result as a value makes errors explicit in the type signature — Result<number, string> tells callers this can fail. Unlike exceptions, errors can't be accidentally ignored. Libraries like neverthrow and fp-ts provide production-ready Result implementations with richer combinators.

📸 Verified Output:


Steps 3–8: Either Type, Error Boundaries, Async Errors, Exhaustive, Retry, Capstone

📸 Verified Output:


Summary

TypeScript error handling is explicit and type-safe. You've covered typed Error subclasses, the Result pattern, error union discriminated types, async error handling, exhaustiveness with never, retry with backoff, and a full product validation pipeline.

Further Reading

Last updated