Designing TypeScript Error Handling Strategies for Scalable Microservices

Learn how to design practical and scalable error handling strategies in TypeScript for microservices, improving reliability and maintainability.

When building microservices in TypeScript, handling errors effectively is crucial for creating scalable and maintainable applications. Unlike monolithic apps, microservices involve multiple independent services communicating over networks, making proper error handling and propagation more important than ever.

In this article, we’ll cover beginner-friendly strategies to design robust error handling in your TypeScript microservices, including custom error classes, centralized error handling, and meaningful error propagation.

### 1. Use Custom Error Classes to Add Context

TypeScript's built-in `Error` class is useful, but creating custom error classes can add more context, like error codes and service-specific details. This makes it easier for upstream services or clients to handle errors appropriately.

typescript
class NotFoundError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "NotFoundError";
  }
}

class ValidationError extends Error {
  constructor(message: string, public errors: string[]) {
    super(message);
    this.name = "ValidationError";
  }
}

### 2. Centralize Error Handling in Your Service

Centralizing error handling allows you to consistently map errors into proper HTTP responses or messages for communication between microservices. For example, an Express middleware can catch thrown errors and return formatted error responses.

typescript
import { Request, Response, NextFunction } from 'express';

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

  if (err instanceof ValidationError) {
    return res.status(400).json({ error: err.message, details: err.errors });
  }

  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
}

### 3. Propagate Meaningful Errors Between Services

When microservices call each other, they should propagate errors with enough detail to enable proper handling without leaking sensitive information. Using error codes and sanitized messages helps achieve this.

typescript
interface ServiceErrorPayload {
  name: string;
  message: string;
  code?: string;
}

// Example: serializing error to send via HTTP or message queue
function serializeError(err: Error): ServiceErrorPayload {
  if (err instanceof NotFoundError) {
    return { name: err.name, message: err.message, code: 'NOT_FOUND' };
  }
  if (err instanceof ValidationError) {
    return { name: err.name, message: err.message, code: 'VALIDATION_ERROR' };
  }

  return { name: 'InternalError', message: 'An unexpected error occurred' };
}

### 4. Use Try-Catch Blocks Wisely

Avoid overusing try-catch blocks. Instead, throw meaningful errors inside your service logic, and use centralized handlers to process them. This approach keeps your code clean and separates business logic from error handling logic.

typescript
async function getUser(id: string) {
  const user = await database.findUserById(id);
  if (!user) {
    throw new NotFoundError(`User with id ${id} not found`);
  }
  return user;
}

// Higher-level function or controller layer
async function handleRequest(id: string) {
  try {
    const user = await getUser(id);
    return user;
  } catch (err) {
    // Could log or transform here if needed
    throw err;
  }
}

### Summary

By designing your TypeScript microservices with custom error classes, centralized error handling, and clean error propagation, you build a scalable system that is easier to debug and maintain. Start simple, and gradually enhance your error strategies as your microservices grow.