TypeScript

Error Handling

Error classes, try-catch patterns, logging

Effective error handling is essential for building robust MCP servers. NitroStack provides exception filters and standardized error patterns to help you handle errors gracefully and provide meaningful feedback to AI models.

Throwing Errors

A tool reports failure by throwing. NitroStack turns the error into an MCP error result, and the model reads its message.

Standard Errors

A plain Error with a clear message covers most cases.

import { ToolDecorator as Tool, ExecutionContext } from '@nitrostack/core';

export class UserTools {
  @Tool({ name: 'get_user' })
  async getUser(input: { userId: string }, ctx: ExecutionContext) {
    const user = await this.userService.findById(input.userId);

    if (!user) {
      throw new Error(`User not found: ${input.userId}`);
    }

    return user;
  }
}

Error with Context

Log the details you'll need later before you throw, and keep the message the model sees short and safe.

@Tool({ name: 'transfer_funds' })
async transferFunds(
  input: { fromAccount: string; toAccount: string; amount: number },
  ctx: ExecutionContext
) {
  const sourceAccount = await this.accountService.findById(input.fromAccount);

  if (!sourceAccount) {
    throw new Error(`Source account not found: ${input.fromAccount}`);
  }

  if (sourceAccount.balance < input.amount) {
    throw new Error(
      `Insufficient funds. Available: ${sourceAccount.balance}, Requested: ${input.amount}`
    );
  }

  return this.accountService.transfer(input);
}

Custom Error Classes

Error classes for your domain carry structured details that filters can turn into consistent responses.

Domain-Specific Errors

Each class names one kind of failure and keeps the fields that describe it.

// errors/not-found.error.ts
export class NotFoundError extends Error {
  public readonly resourceType: string;
  public readonly resourceId: string;

  constructor(resourceType: string, resourceId: string) {
    super(`${resourceType} not found: ${resourceId}`);
    this.name = 'NotFoundError';
    this.resourceType = resourceType;
    this.resourceId = resourceId;
  }
}

// errors/validation.error.ts
export class ValidationError extends Error {
  public readonly field: string;
  public readonly value: unknown;
  public readonly constraint: string;

  constructor(field: string, value: unknown, constraint: string) {
    super(`Validation failed for ${field}: ${constraint}`);
    this.name = 'ValidationError';
    this.field = field;
    this.value = value;
    this.constraint = constraint;
  }
}

// errors/authorization.error.ts
export class AuthorizationError extends Error {
  public readonly requiredPermission: string;
  public readonly userId?: string;

  constructor(requiredPermission: string, userId?: string) {
    super(`Access denied. Required permission: ${requiredPermission}`);
    this.name = 'AuthorizationError';
    this.requiredPermission = requiredPermission;
    this.userId = userId;
  }
}

// errors/business-rule.error.ts
export class BusinessRuleError extends Error {
  public readonly rule: string;
  public readonly context: Record<string, unknown>;

  constructor(rule: string, message: string, context: Record<string, unknown> = {}) {
    super(message);
    this.name = 'BusinessRuleError';
    this.rule = rule;
    this.context = context;
  }
}

Using Custom Errors

Throw the specific class, so a filter or the caller can tell failures apart.

import { NotFoundError, ValidationError, BusinessRuleError } from './errors/index.js';

@Tool({ name: 'update_order' })
async updateOrder(
  input: { orderId: string; status: string },
  ctx: ExecutionContext
) {
  const order = await this.orderService.findById(input.orderId);

  if (!order) {
    throw new NotFoundError('Order', input.orderId);
  }

  const validStatuses = ['pending', 'processing', 'shipped', 'delivered'];
  if (!validStatuses.includes(input.status)) {
    throw new ValidationError(
      'status',
      input.status,
      `Must be one of: ${validStatuses.join(', ')}`
    );
  }

  if (order.status === 'delivered' && input.status !== 'delivered') {
    throw new BusinessRuleError(
      'ORDER_ALREADY_DELIVERED',
      'Cannot change status of a delivered order',
      { orderId: order.id, currentStatus: order.status }
    );
  }

  return this.orderService.updateStatus(input.orderId, input.status);
}

Exception Filters

An exception filter catches errors from a tool and decides what the client receives. See the filters reference for the interface.

Creating an Exception Filter

A filter implements catch(), which receives the error and the context and returns the response.

import { ExceptionFilter, ExceptionFilterInterface, ExecutionContext } from '@nitrostack/core';

@ExceptionFilter()
export class GlobalExceptionFilter implements ExceptionFilterInterface {
  catch(exception: unknown, context: ExecutionContext): any {
    const timestamp = new Date().toISOString();
    const requestId = context.metadata?.requestId || context.requestId;

    // Log the error
    context.logger.error('Exception caught', {
      requestId,
      toolName: context.toolName,
      error: this.serializeError(exception)
    });

    // Handle specific error types
    if (exception instanceof NotFoundError) {
      return {
        error: true,
        code: 'NOT_FOUND',
        message: exception.message,
        details: {
          resourceType: exception.resourceType,
          resourceId: exception.resourceId
        },
        timestamp,
        requestId
      };
    }

    if (exception instanceof ValidationError) {
      return {
        error: true,
        code: 'VALIDATION_ERROR',
        message: exception.message,
        details: {
          field: exception.field,
          constraint: exception.constraint
        },
        timestamp,
        requestId
      };
    }

    if (exception instanceof AuthorizationError) {
      return {
        error: true,
        code: 'FORBIDDEN',
        message: 'Access denied',
        timestamp,
        requestId
      };
    }

    if (exception instanceof BusinessRuleError) {
      return {
        error: true,
        code: exception.rule,
        message: exception.message,
        details: exception.context,
        timestamp,
        requestId
      };
    }

    // Generic error handling
    return {
      error: true,
      code: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred',
      timestamp,
      requestId
    };
  }

  private serializeError(error: unknown): Record<string, unknown> {
    if (error instanceof Error) {
      return {
        name: error.name,
        message: error.message,
        stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
      };
    }
    return { value: String(error) };
  }
}

Using Exception Filters

Attach a filter with @UseFilters.

import { UseFilters } from '@nitrostack/core';
import { GlobalExceptionFilter } from './filters/global-exception.filter.js';

@Tool({ name: 'risky_operation' })
@UseFilters(GlobalExceptionFilter)
async riskyOperation(input: RiskyInput, ctx: ExecutionContext) {
  // Errors are caught and formatted by the filter
  return this.riskyService.execute(input);
}

Specialized Filters

A filter can handle one kind of error, such as database failures, and rethrow everything else.

@ExceptionFilter()
export class DatabaseExceptionFilter implements ExceptionFilterInterface {
  catch(exception: unknown, context: ExecutionContext): any {
    if (this.isDatabaseError(exception)) {
      context.logger.error('Database error', {
        code: exception.code,
        message: exception.message
      });

      // Handle specific database errors
      if (exception.code === 'UNIQUE_VIOLATION') {
        return {
          error: true,
          code: 'DUPLICATE_ENTRY',
          message: 'A record with this value already exists'
        };
      }

      if (exception.code === 'FOREIGN_KEY_VIOLATION') {
        return {
          error: true,
          code: 'REFERENCE_ERROR',
          message: 'Referenced record does not exist'
        };
      }

      return {
        error: true,
        code: 'DATABASE_ERROR',
        message: 'A database error occurred'
      };
    }

    // Re-throw non-database errors
    throw exception;
  }

  private isDatabaseError(error: unknown): error is DatabaseError {
    return error instanceof Error && 'code' in error;
  }
}

Error Response Patterns

Return errors in one shape across your server, so clients and models can handle them the same way.

Standardized Error Response

A shared shape with a machine-readable code and a human-readable message.

interface ErrorResponse {
  error: true;
  code: string;
  message: string;
  details?: Record<string, unknown>;
  timestamp: string;
  requestId?: string;
}

// Example responses:

// Not found
{
  error: true,
  code: 'NOT_FOUND',
  message: 'User not found: usr_abc123',
  details: { resourceType: 'User', resourceId: 'usr_abc123' },
  timestamp: '2024-01-15T10:30:00Z',
  requestId: 'req_xyz789'
}

// Validation error
{
  error: true,
  code: 'VALIDATION_ERROR',
  message: 'Validation failed for email: Must be a valid email address',
  details: { field: 'email', constraint: 'Must be a valid email address' },
  timestamp: '2024-01-15T10:30:00Z',
  requestId: 'req_xyz789'
}

// Business rule violation
{
  error: true,
  code: 'INSUFFICIENT_FUNDS',
  message: 'Insufficient funds for transfer',
  details: { available: 100.00, requested: 150.00 },
  timestamp: '2024-01-15T10:30:00Z',
  requestId: 'req_xyz789'
}

Error Codes Enumeration

Keep error codes in one place, so every tool uses the same names.

export const ErrorCodes = {
  // Client errors (4xx equivalent)
  VALIDATION_ERROR: 'VALIDATION_ERROR',
  NOT_FOUND: 'NOT_FOUND',
  UNAUTHORIZED: 'UNAUTHORIZED',
  FORBIDDEN: 'FORBIDDEN',
  CONFLICT: 'CONFLICT',
  RATE_LIMITED: 'RATE_LIMITED',

  // Business errors
  INSUFFICIENT_FUNDS: 'INSUFFICIENT_FUNDS',
  ORDER_ALREADY_SHIPPED: 'ORDER_ALREADY_SHIPPED',
  INVENTORY_EXHAUSTED: 'INVENTORY_EXHAUSTED',

  // Server errors (5xx equivalent)
  INTERNAL_ERROR: 'INTERNAL_ERROR',
  DATABASE_ERROR: 'DATABASE_ERROR',
  EXTERNAL_SERVICE_ERROR: 'EXTERNAL_SERVICE_ERROR'
} as const;

Logging Errors

Log enough to debug a failure without leaking secrets to the model.

Structured Error Logging

Use the context's logger with fields, not string concatenation, so logs stay searchable.

@Tool({ name: 'process_payment' })
async processPayment(input: PaymentInput, ctx: ExecutionContext) {
  try {
    return await this.paymentService.process(input);
  } catch (error) {
    // Log with full context
    ctx.logger.error('Payment processing failed', {
      requestId: ctx.requestId,
      userId: ctx.auth?.subject,
      amount: input.amount,
      error: {
        name: error instanceof Error ? error.name : 'Unknown',
        message: error instanceof Error ? error.message : String(error),
        stack: error instanceof Error ? error.stack : undefined
      }
    });

    // Re-throw for filter handling
    throw error;
  }
}

Error Monitoring Integration

Send errors to a monitoring service from one injectable reporter.

import { Injectable } from '@nitrostack/core';

@Injectable()
export class ErrorReporter {
  constructor(private config: ConfigService) {}

  report(error: Error, context: Record<string, unknown>): void {
    // Log locally
    console.error('Error reported:', error.message, context);

    // Send to monitoring service in production
    if (this.config.get('NODE_ENV') === 'production') {
      // Integration with error tracking services
      // e.g., Sentry, DataDog, etc.
    }
  }
}

On this page