Skip to main content

Module 28: Error Handling Best Practices

Master error handling strategies in TypeScript for building resilient applications with proper error types and recovery mechanisms.


1. Custom Error Classes

class AppError extends Error {
constructor(
public message: string,
public statusCode: number = 500,
public isOperational: boolean = true
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}

class ValidationError extends AppError {
constructor(message: string) {
super(message, 400);
}
}

class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 404);
}
}

class UnauthorizedError extends AppError {
constructor(message: string = "Unauthorized") {
super(message, 401);
}
}

2. Result Type Pattern

type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };

function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return { success: false, error: "Division by zero" };
}
return { success: true, value: a / b };
}

const result = divide(10, 2);
if (result.success) {
console.log("Result:", result.value);
} else {
console.error("Error:", result.error);
}

3. Option/Maybe Type

type Option<T> = Some<T> | None;

class Some<T> {
constructor(private value: T) {}

map<U>(fn: (value: T) => U): Option<U> {
return new Some(fn(this.value));
}

getOrElse(_default: T): T {
return this.value;
}

isSome(): this is Some<T> {
return true;
}
}

class None {
map<U>(_fn: (value: never) => U): Option<U> {
return new None();
}

getOrElse<T>(defaultValue: T): T {
return defaultValue;
}

isSome(): this is Some<never> {
return false;
}
}

function findUser(id: number): Option<User> {
const user = database.find(id);
return user ? new Some(user) : new None();
}

4. Try-Catch Wrapper

async function tryCatch<T>(
promise: Promise<T>
): Promise<[Error, null] | [null, T]> {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error as Error, null];
}
}

// Usage
const [error, user] = await tryCatch(fetchUser(1));
if (error) {
console.error("Failed to fetch user:", error);
return;
}
console.log("User:", user);

5. Error Boundary (React)

interface Props {
children: React.ReactNode;
}

interface State {
hasError: boolean;
error?: Error;
}

class ErrorBoundary extends React.Component<Props, State> {
state: State = { hasError: false };

static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error("Error caught:", error, errorInfo);
}

render() {
if (this.state.hasError) {
return <div>Something went wrong: {this.state.error?.message}</div>;
}

return this.props.children;
}
}

6. Global Error Handler

// Express error handler
import { Request, Response, NextFunction } from "express";

function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (err instanceof AppError) {
res.status(err.statusCode).json({
status: "error",
message: err.message
});
return;
}

console.error("Unexpected error:", err);
res.status(500).json({
status: "error",
message: "Internal server error"
});
}

app.use(errorHandler);

7. Validation with Errors

interface ValidationResult {
valid: boolean;
errors: Record<string, string[]>;
}

class Validator<T> {
private errors: Record<string, string[]> = {};

required(field: keyof T, value: any, message?: string): this {
if (!value) {
this.addError(field as string, message || `${String(field)} is required`);
}
return this;
}

minLength(field: keyof T, value: string, min: number, message?: string): this {
if (value.length < min) {
this.addError(
field as string,
message || `${String(field)} must be at least ${min} characters`
);
}
return this;
}

private addError(field: string, message: string): void {
if (!this.errors[field]) {
this.errors[field] = [];
}
this.errors[field].push(message);
}

validate(): ValidationResult {
return {
valid: Object.keys(this.errors).length === 0,
errors: this.errors
};
}
}

// Usage
interface User {
name: string;
email: string;
}

const validator = new Validator<User>();
const result = validator
.required("name", data.name)
.required("email", data.email)
.minLength("name", data.name, 3)
.validate();

8. Retry Logic with Backoff

async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) {
throw error;
}

const delay = baseDelay * Math.pow(2, attempt);
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}

throw new Error("Max retries reached");
}

// Usage
const data = await retryWithBackoff(() => fetchData(), 5, 1000);

9. Circuit Breaker

enum CircuitState {
CLOSED,
OPEN,
HALF_OPEN
}

class CircuitBreaker {
private state = CircuitState.CLOSED;
private failureCount = 0;
private lastFailureTime?: number;

constructor(
private threshold: number = 5,
private timeout: number = 60000
) {}

async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime! > this.timeout) {
this.state = CircuitState.HALF_OPEN;
} else {
throw new Error("Circuit breaker is OPEN");
}
}

try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}

private onSuccess(): void {
this.failureCount = 0;
this.state = CircuitState.CLOSED;
}

private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();

if (this.failureCount >= this.threshold) {
this.state = CircuitState.OPEN;
}
}
}

10. Graceful Degradation

class FeatureService {
async getData(): Promise<Data> {
try {
return await this.fetchFromPrimary();
} catch (error) {
console.warn("Primary service failed, trying fallback...");

try {
return await this.fetchFromFallback();
} catch (fallbackError) {
console.error("Fallback also failed, using cache...");
return this.getFromCache();
}
}
}

private async fetchFromPrimary(): Promise<Data> {
// Primary data source
}

private async fetchFromFallback(): Promise<Data> {
// Fallback data source
}

private getFromCache(): Data {
// Cache or default data
}
}

Key Takeaways

Custom error classes for specific errors
Result type for explicit error handling
Try-catch wrappers for async operations
Error boundaries in React
Retry logic for transient failures
Circuit breaker for fault tolerance


Practice Exercises

Exercise 1: API Error Handler

Create a comprehensive error handling system for REST APIs.

Exercise 2: Error Logger

Build an error logging service with different severity levels.


Next Steps

In Module 29, we'll explore TypeScript with GraphQL for type-safe API development.