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