Error Handling & Validation
๐ก๏ธ Error Handling & Validation
โ ๏ธ 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"); } }
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.
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.
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.
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.