Skip to content

Обработка ошибок

Иерархия классов ошибок

typescript
// core/errors/base.error.ts
export abstract class DomainError extends Error {
  abstract readonly code: string;
  abstract readonly statusCode: number;

  constructor(message: string, public readonly details?: Record<string, unknown>) {
    super(message);
    this.name = this.constructor.name;
  }
}

// core/errors/not-found.error.ts
export class NotFoundError extends DomainError {
  readonly code = 'NOT_FOUND';
  readonly statusCode = 404;

  constructor(entity: string, id: string) {
    super(`${entity} with id ${id} not found`, { entity, id });
  }
}

// core/errors/validation.error.ts
export class ValidationError extends DomainError {
  readonly code = 'VALIDATION_ERROR';
  readonly statusCode = 400;

  constructor(message: string, public readonly fields: Record<string, string[]>) {
    super(message, { fields });
  }
}

// core/errors/business.error.ts
export class BusinessRuleError extends DomainError {
  readonly code: string;
  readonly statusCode = 422;

  constructor(code: string, message: string, details?: Record<string, unknown>) {
    super(message, details);
    this.code = code;
  }
}

// modules/mlm/domain/errors/
export class InsufficientBalanceError extends BusinessRuleError {
  constructor(available: number, requested: number) {
    super(
      'INSUFFICIENT_BALANCE',
      `Insufficient balance: available ${available}, requested ${requested}`,
      { available, requested }
    );
  }
}

export class KycRequiredError extends BusinessRuleError {
  constructor(requiredLevel: string, currentLevel: string) {
    super(
      'KYC_REQUIRED',
      `KYC level ${requiredLevel} required, current level is ${currentLevel}`,
      { requiredLevel, currentLevel }
    );
  }
}

Таксономия кодов ошибок

typescript
// Формат кода ошибки: MODULE_CATEGORY_SPECIFIC
const ERROR_CODES = {
  // Ошибки аутентификации (1xxx)
  AUTH_INVALID_CREDENTIALS: 'AUTH_001',
  AUTH_TOKEN_EXPIRED: 'AUTH_002',
  AUTH_2FA_REQUIRED: 'AUTH_003',
  AUTH_2FA_INVALID: 'AUTH_004',
  AUTH_ACCOUNT_LOCKED: 'AUTH_005',

  // Ошибки пользователей (2xxx)
  USER_NOT_FOUND: 'USER_001',
  USER_EMAIL_EXISTS: 'USER_002',
  USER_PHONE_EXISTS: 'USER_003',

  // Ошибки KYC (3xxx)
  KYC_REQUIRED: 'KYC_001',
  KYC_PENDING: 'KYC_002',
  KYC_REJECTED: 'KYC_003',
  KYC_DOCUMENT_INVALID: 'KYC_004',

  // Ошибки MLM (4xxx)
  MLM_PARTNER_NOT_FOUND: 'MLM_001',
  MLM_INSUFFICIENT_BALANCE: 'MLM_002',
  MLM_PAYOUT_LIMIT_EXCEEDED: 'MLM_003',
  MLM_RANK_REQUIREMENT_NOT_MET: 'MLM_004',
  MLM_REFERRAL_SELF_REFERENCE: 'MLM_005',

  // Ошибки заказов (5xxx)
  ORDER_NOT_FOUND: 'ORDER_001',
  ORDER_ALREADY_PAID: 'ORDER_002',
  ORDER_CANCELLED: 'ORDER_003',
  ORDER_PRODUCT_OUT_OF_STOCK: 'ORDER_004',

  // Ошибки оплаты (6xxx)
  PAYMENT_FAILED: 'PAYMENT_001',
  PAYMENT_DECLINED: 'PAYMENT_002',
  PAYMENT_TIMEOUT: 'PAYMENT_003',

  // Ошибки инвестиций (7xxx)
  INVESTMENT_STRATEGY_CLOSED: 'INV_001',
  INVESTMENT_MIN_AMOUNT: 'INV_002',
  INVESTMENT_MAX_AMOUNT: 'INV_003',
} as const;

Стандартный формат ответа об ошибке

typescript
// Все ошибки API следуют этому формату
interface ErrorResponse {
  success: false;
  error: {
    code: string;           // Машиночитаемый код (например, 'MLM_002')
    message: string;        // Человекочитаемое сообщение
    details?: unknown;      // Дополнительный контекст
    timestamp: string;      // ISO 8601
    requestId: string;      // Для трассировки
  };
}

Примеры ответов

400 Bad Request (Валидация)

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": {
      "fields": {
        "email": ["Invalid email format"],
        "amount": ["Must be greater than 0"]
      }
    },
    "timestamp": "2024-01-15T10:30:00Z",
    "requestId": "req_abc123"
  }
}

422 Unprocessable Entity (Бизнес-правило)

json
{
  "success": false,
  "error": {
    "code": "MLM_002",
    "message": "Insufficient balance: available 500.00, requested 1000.00",
    "details": {
      "available": 500.00,
      "requested": 1000.00,
      "currency": "RUB"
    },
    "timestamp": "2024-01-15T10:30:00Z",
    "requestId": "req_abc123"
  }
}

500 Internal Server Error

json
{
  "success": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An unexpected error occurred",
    "timestamp": "2024-01-15T10:30:00Z",
    "requestId": "req_abc123"
  }
}

Глобальный фильтр исключений

typescript
// infrastructure/http/filters/global-exception.filter.ts
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  constructor(private readonly logger: Logger) {}

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const requestId = request.headers['x-request-id'] ?? generateRequestId();

    let statusCode = 500;
    let errorResponse: ErrorResponse;

    if (exception instanceof DomainError) {
      statusCode = exception.statusCode;
      errorResponse = {
        success: false,
        error: {
          code: exception.code,
          message: exception.message,
          details: exception.details,
          timestamp: new Date().toISOString(),
          requestId,
        },
      };

      // Логирование бизнес-ошибок на уровне info
      this.logger.info('Business error', {
        code: exception.code,
        message: exception.message,
        requestId,
      });

    } else if (exception instanceof HttpException) {
      statusCode = exception.getStatus();
      const exceptionResponse = exception.getResponse();

      errorResponse = {
        success: false,
        error: {
          code: `HTTP_${statusCode}`,
          message: typeof exceptionResponse === 'string'
            ? exceptionResponse
            : (exceptionResponse as any).message,
          timestamp: new Date().toISOString(),
          requestId,
        },
      };

    } else {
      // Непредвиденные ошибки - логируем полный стектрейс
      this.logger.error('Unexpected error', {
        error: exception instanceof Error ? exception.stack : String(exception),
        requestId,
        path: request.url,
        method: request.method,
      });

      errorResponse = {
        success: false,
        error: {
          code: 'INTERNAL_ERROR',
          message: 'An unexpected error occurred',
          timestamp: new Date().toISOString(),
          requestId,
        },
      };
    }

    response.status(statusCode).json(errorResponse);
  }
}

Связанные документы