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.