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.0,
      "requested": 1000.0,
      "currency": "USD"
    },
    "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);
  }
}

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