Обработка ошибок
Иерархия классов ошибок
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);
}
}