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";
}
}
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 {
const user = await fetchUser(id);
} catch (error) {
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 };
}
}
const result = parseJSON<User>(jsonString);
if (result.ok) {
console.log("Parsed:", result.value);
} 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:
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 {
if (id <= 0) {
throw new ValidationError("id", "Must be positive");
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch(`/api/users/${id}`, {
signal: controller.signal
});
clearTimeout(timeout);
if (!response.ok) {
throw new NetworkError(response.status, response.statusText);
}
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.