Designing Robust Error Handling Systems in TypeScript for Scalable Applications

Learn how to build reliable and maintainable error handling systems in TypeScript to improve the stability and user experience of your scalable applications.

Error handling is a crucial aspect of building scalable applications. In TypeScript, designing a robust error handling system helps you manage unexpected situations gracefully, maintain clean code, and improve the overall stability of your app. This beginner-friendly guide introduces you to practical techniques for handling errors effectively in TypeScript.

### Why Robust Error Handling Matters Errors can occur anywhere—network failures, invalid user inputs, or unexpected bug triggers. Without a centralized and thoughtful error handling system, your application might crash, produce confusing messages, or be difficult to maintain.

### Using Custom Error Classes TypeScript’s ability to extend built-in classes allows you to create custom error types, giving you better control and clarity.

typescript
class AppError extends Error {
  public readonly isOperational: boolean;
  public readonly statusCode: number;

  constructor(message: string, statusCode = 500, isOperational = true) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = isOperational;
    Object.setPrototypeOf(this, new.target.prototype); // Restore prototype chain
    Error.captureStackTrace(this);
  }
}

// Example usage:
const dbError = new AppError('Database connection failed', 503);
console.log(dbError.message); // 'Database connection failed'
console.log(dbError.statusCode); // 503

### Centralizing Error Handling Logic Create a single place to handle all errors, such as middleware in Express or a global error handler in other environments. This centralization helps maintain readability and consistency.

typescript
import express, { Request, Response, NextFunction } from 'express';
const app = express();

// Middleware to simulate an error
app.get('/error', (req: Request, res: Response, next: NextFunction) => {
  next(new AppError('Something went wrong!', 400));
});

// Central error handler middleware
app.use((err: AppError, req: Request, res: Response, next: NextFunction) => {
  if (err.isOperational) {
    res.status(err.statusCode).json({ status: 'error', message: err.message });
  } else {
    // Programming or unknown error, don't leak details
    console.error('Unexpected Error:', err);
    res.status(500).json({ status: 'error', message: 'Internal Server Error' });
  }
});

### Handling Asynchronous Errors In TypeScript, many functions work asynchronously and can throw errors inside promises or async/await. Make sure to catch errors properly to avoid unhandled exceptions.

typescript
async function fetchUser(userId: string) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    if (!response.ok) {
      throw new AppError('Failed to fetch user', response.status);
    }
    return await response.json();
  } catch (error) {
    if (error instanceof AppError) {
      throw error; // Re-throw known AppErrors
    }
    throw new AppError('Unknown error fetching user', 500, false);
  }
}

### Best Practices Summary: - Use custom error classes to represent different error kinds. - Create centralized error handling middleware or functions. - Always handle async errors with try/catch. - Distinguish between operational (expected) errors and programmer errors. - Log errors internally, but provide user-friendly messages externally.

By designing your error handling this way, your TypeScript application will be more maintainable, secure, and ready to scale as it grows.