Error Handling & Validation

TypeScript Intermediate — Error Handling & Validation
TypeScript Intermediate
Course 2 · Chapter 8 · Error Handling & Validation

🛡️ Error Handling & Validation

Errors are inevitable. This chapter teaches you to design robust error hierarchies, validate data defensively, handle errors gracefully, and use TypeScript's type system to catch errors at compile time. Good error handling separates amateur from professional code.

⚠️ Custom Error Classes: Structured Errors

Extend Error to create domain-specific error types:

class ValidationError extends Error { constructor(public field: string, message: string) { super(message); this.name = "ValidationError"; } } class NetworkError extends Error { constructor(public statusCode: number, message: string) { super(message); this.name = "NetworkError"; } } class DatabaseError extends Error { constructor(public query: string, message: string) { super(message); this.name = "DatabaseError"; } } // Usage: throw with context throw new ValidationError("email", "Invalid email format"); throw new NetworkError(404, "User not found");

Structured Data

Attach context: which field failed, what status code, what query.

Error.name

Set this.name so you can identify error types later.

Type Discrimination

Use instanceof or error.name to handle different errors differently.

Call super()

Always call super(message) to set the error message.

🎯 Handling Errors: Try-Catch Patterns

try { // Risky operation const user = await fetchUser(id); } catch (error) { // Always type-guard: error could be anything if (error instanceof ValidationError) { console.error(`Field validation failed: ${error.field}`); } else if (error instanceof NetworkError) { console.error(`Server error: ${error.statusCode}`); } else if (error instanceof Error) { console.error(`Unexpected: ${error.message}`); } else { console.error("Unknown error"); } }
⚠️ Important: In TypeScript, the caught error is of type unknown, not Error. Always type-guard before using it.

✅ Validation: Fail Fast or Collect All?

Two Validation Strategies

Fail Fast (throw on first error)
function validate(data: User) { if (!data.email) { throw new ValidationError("email", "Required"); } if (data.age < 18) { throw new ValidationError("age", "Must be 18+"); } }
Collect All (gather all errors)
function validate(data: User): ValidationError[] { const errors: ValidationError[] = []; if (!data.email) { errors.push(new ValidationError("email", "Required")); } if (data.age < 18) { errors.push(new ValidationError("age", "Must be 18+")); } return errors; }

🔄 Result Type: No Exceptions

Instead of throwing, return success or error as data:

type Result<T, E = Error> = | { ok: true; value: T } | { ok: false; error: E }; async function parseJSON<T>(text: string): Result<T, SyntaxError> { try { const value = JSON.parse(text) as T; return { ok: true, value }; } catch (error) { return { ok: false, error: error as SyntaxError }; } } // Usage: no try-catch needed const result = parseJSON<User>(jsonString); if (result.ok) { console.log("Parsed:", result.value); // ✅ User type } else { console.error("Parse failed:", result.error.message); }

🔍 Exhaustiveness: Catch Missing Cases

Use discriminated unions + the never type to ensure all cases are handled:

type FileOperation = | { type: "read"; path: string } | { type: "write"; path: string; content: string } | { type: "delete"; path: string }; function handleOperation(op: FileOperation): void { switch (op.type) { case "read": console.log(`Reading ${op.path}`); break; case "write": console.log(`Writing to ${op.path}`); break; case "delete": console.log(`Deleting ${op.path}`); break; default: // If you forget a case, TypeScript errors here const _exhaustive: never = op; return _exhaustive; } }

The trick: If you add a new case to the union but don't handle it, the op value has type never, which causes a compile error. Forces you to update all handlers.

🏗️ Real-World Pattern: Robust API Call

Handle All Failure Modes

async function fetchUserSafe(id: number) { try { // 1. Validate input if (id <= 0) { throw new ValidationError("id", "Must be positive"); } // 2. Network call with timeout const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const response = await fetch(`/api/users/${id}`, { signal: controller.signal }); clearTimeout(timeout); // 3. Check status if (!response.ok) { throw new NetworkError(response.status, response.statusText); } // 4. Parse and validate const data = await response.json(); if (!data.id || !data.name) { throw new Error("Invalid response shape"); } return { ok: true, data }; } catch (error) { if (error instanceof ValidationError) { return { ok: false, reason: "validation", error }; } else if (error instanceof NetworkError) { return { ok: false, reason: "network", error }; } else { return { ok: false, reason: "unknown", error }; } } }

💻 Coding Challenges

Challenge 1: Custom Error Hierarchy

Create an error base class and 3 subclasses (ValidationError, NetworkError, AuthError). Write a function that catches each type and handles differently.

Goal: Practice structured error handling.

→ Solution

Challenge 2: Validation with Error Collection

Write a validator that collects all errors instead of throwing on first. Return an array of errors or empty array on success.

Goal: Implement collect-all validation pattern.

→ Solution

Challenge 3: Result Type & Exhaustiveness

Implement a Result<T, E> type. Add a discriminated union operation type. Use exhaustiveness checking to ensure all operations are handled.

Goal: Combine Result type with exhaustiveness checking.

→ Solution

⚠️ Gotcha: The unknown Caught Error

In TypeScript, catch(error) has type unknown, not Error. Even if you only throw Error subclasses, something could throw a string or null. Always guard with instanceof or typeof before using the error.

🎯 What's Next

With error handling mastered, we'll explore the final chapter: Design Patterns in TypeScript — bringing together everything you've learned to build robust, scalable applications.