Handle errors in a type-safe way with TypeScript.
Result Pattern
Instead of throwing, return a result object.
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function divide(a: number, b: number): Result<number> {
if (b === 0) {
return { success: false, error: new Error("Division by zero") };
}
return { success: true, data: a / b };
}
const result = divide(10, 2);
if (result.success) {
console.log(result.data); // TypeScript knows data exists
} else {
console.error(result.error); // TypeScript knows error exists
}
Option/Maybe Pattern
Represent values that might not exist.
type Option<T> = T | null;
function findUser(id: string): Option<User> {
// Might return null
return users.find(u => u.id === id) ?? null;
}
const user = findUser("123");
if (user) {
// TypeScript knows user is not null
console.log(user.name);
}
Better Option Type
type Some<T> = { kind: "some"; value: T };
type None = { kind: "none" };
type Option<T> = Some<T> | None;
function findUser(id: string): Option<User> {
const user = users.find(u => u.id === id);
return user ? { kind: "some", value: user } : { kind: "none" };
}
const result = findUser("123");
if (result.kind === "some") {
console.log(result.value.name); // TypeScript knows value exists
}
Try-Catch with Types
TypeScript doesn't type catch blocks well, so be explicit.
function riskyOperation(): string {
// Might throw
return "result";
}
try {
const result = riskyOperation();
// result is string
} catch (error) {
// error is unknown, not any
if (error instanceof Error) {
console.error(error.message);
} else {
console.error("Unknown error");
}
}
Custom Error Classes
Create typed error classes.
class ValidationError extends Error {
constructor(
message: string,
public field: string,
public value: any
) {
super(message);
this.name = "ValidationError";
}
}
class NetworkError extends Error {
constructor(
message: string,
public statusCode: number
) {
super(message);
this.name = "NetworkError";
}
}
type AppError = ValidationError | NetworkError;
function handleError(error: AppError) {
if (error instanceof ValidationError) {
console.error(`Validation failed for ${error.field}: ${error.message}`);
} else if (error instanceof NetworkError) {
console.error(`Network error ${error.statusCode}: ${error.message}`);
}
}
Assertion Functions for Errors
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function process(value: string | null) {
assert(value !== null, "Value cannot be null");
// TypeScript knows value is string here
console.log(value.length);
}
Exercises
Exercise 1: Result Pattern
// Create a function that:
// - Takes a URL string
// - Returns Result<string> (the fetched data or error)
// - Use the Result type to handle success/failure
Exercise 2: Option Type
// Create a function 'getProperty' that:
// - Takes an object and a key
// - Returns Option<T> for the property value
// - Returns None if property doesn't exist
Exercise 3: Custom Errors
// Create custom error classes:
// - ParseError
// - NotFoundError
// - PermissionError
// Create a function that throws appropriate errors
Exercise 4: Error Handling Function
// Create a function 'safeExecute' that:
// - Takes a function that might throw
// - Returns Result<T, Error>
// - Catches errors and returns them in Result format
Exercise 5: Error Union
// Create a union type for different error types
// Create a handler that uses type narrowing to handle each error type
Best Practices
- Use Result pattern - More type-safe than exceptions
- Use Option for nullable values - Explicit about absence
- Create custom error classes - Better error handling
- Type catch blocks - Don't assume error is Error
- Document error conditions - Make them explicit in types
Common Mistakes
- Using any for errors
// ❌ Bad
catch (error: any) {
console.log(error.message); // Might not exist
}
// ✅ Good
catch (error: unknown) {
if (error instanceof Error) {
console.log(error.message);
}
}
- Not handling all error cases
// ❌ Missing error handling
function process(data: string) {
return JSON.parse(data); // Might throw
}
// ✅ Handle errors
function process(data: string): Result<any> {
try {
return { success: true, data: JSON.parse(data) };
} catch (error) {
return { success: false, error: error as Error };
}
}
- Throwing instead of returning errors
// ❌ Throwing loses type information
function divide(a: number, b: number): number {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
// ✅ Return Result
function divide(a: number, b: number): Result<number> {
if (b === 0) {
return { success: false, error: new Error("Division by zero") };
}
return { success: true, data: a / b };
}
Next Steps
Congratulations! You've completed the Intermediate level.
Proceed to Advanced Level to learn about advanced type system features.