import { InsufficientBalanceException } from './common/exceptions';
throw new InsufficientBalanceException('BTC', 0.5, 0.2);import { ApiBalanceErrorResponses } from './common/decorators/swagger-error-responses.decorator';
@Post('deduct')
@ApiBalanceErrorResponses()
deductBalance(@Body() dto: DeductBalanceDto) { }it('should throw InsufficientBalanceException', async () => {
const response = await request(app.getHttpServer())
.post('/api/balance/deduct')
.send({ asset: 'BTC', amount: 100 });
expect(response.status).toBe(400);
expect(response.body.error.code).toBe('INSUFFICIENT_BALANCE');
});| Exception | HTTP | Code | When to Use |
|---|---|---|---|
InsufficientBalanceException |
400 | INSUFFICIENT_BALANCE |
User balance too low |
InvalidTradeException |
400 | INVALID_TRADE |
Invalid trade parameters |
ResourceNotFoundException |
404 | RESOURCE_NOT_FOUND |
Resource not found |
UnauthorizedAccessException |
403 | UNAUTHORIZED_ACCESS |
Permission denied |
AuthenticationFailedException |
401 | AUTHENTICATION_FAILED |
Auth failed |
RateLimitExceededException |
429 | RATE_LIMIT_EXCEEDED |
Too many requests |
ValidationException |
400 | VALIDATION_FAILED |
Input validation error |
ConflictException |
409 | CONFLICT |
Resource already exists |
DatabaseException |
500 | DATABASE_ERROR |
DB operation failed |
ExternalServiceException |
502 | EXTERNAL_SERVICE_ERROR |
External service error |
TimeoutException |
408 | OPERATION_TIMEOUT |
Operation timed out |
InvalidStateException |
400 | INVALID_STATE_TRANSITION |
Invalid state change |
ResourceLockedException |
423 | RESOURCE_LOCKED |
Resource locked |
if (!balance) {
throw new ResourceNotFoundException('Balance', userId);
}
if (balance.amount < amount) {
throw new InsufficientBalanceException(asset, amount, balance.amount);
}if (!email || !password) {
throw new ValidationException({
email: !email ? ['Email is required'] : [],
password: !password ? ['Password is required'] : [],
});
}try {
return await this.repository.save(entity);
} catch (error) {
throw new DatabaseException(
'save',
error instanceof Error ? error.message : 'Unknown error',
);
}try {
await this.balanceService.deductBalance(userId, asset, amount);
// throws InsufficientBalanceException - will be caught by filter
} catch (error) {
if (error instanceof InsufficientBalanceException) {
throw error;
}
throw new DatabaseException('execute', error.message);
}@ApiErrorResponses()
endpoint() { }@ApiBalanceErrorResponses()
deductBalance() { }@ApiTradeErrorResponses()
executeTrade() { }@ApiAuthErrorResponses()
login() { }describe('Service', () => {
it('should throw specific exception', async () => {
expect(async () => {
await service.operation();
}).rejects.toThrow(SpecificException);
});
it('should return proper error response', async () => {
const res = await request(app.getHttpServer())
.post('/api/endpoint')
.send(invalidData);
expect(res.status).toBe(400);
expect(res.body.error.code).toBe('ERROR_CODE');
expect(res.body.error).toHaveProperty('message');
expect(res.body.error).toHaveProperty('timestamp');
});
});{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance for BTC. Required: 0.5, Available: 0.2",
"timestamp": "2024-01-29T10:30:00.000Z"
},
"metadata": {
"asset": "BTC",
"required": 0.5,
"available": 0.2
}
}{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"timestamp": "2024-01-29T10:30:00.000Z",
"validationErrors": {
"email": ["Email must be valid"],
"password": ["Password must be at least 8 characters"]
}
}
}{
"success": false,
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User with identifier 123 not found",
"timestamp": "2024-01-29T10:30:00.000Z"
},
"metadata": {
"resourceType": "User",
"identifier": 123
}
}import { ErrorCategory, categorizeError } from './common/exceptions/error-codes';
const category = categorizeError('AUTH_001'); // ErrorCategory.AUTHENTICATIONCategories:
AUTHENTICATION- Auth failures (401)AUTHORIZATION- Permission denied (403)VALIDATION- Input validation (400)NOT_FOUND- Resource not found (404)CONFLICT- Resource conflicts (409)BUSINESS_LOGIC- Business rule violations (400)DATABASE- DB errors (500)EXTERNAL_SERVICE- External API errors (502)RATE_LIMIT- Rate limiting (429)TIMEOUT- Operation timeout (408)INTERNAL- Unexpected errors (500)
- Throw custom exceptions with context
- Log errors with ErrorLoggerService
- Use validation exceptions for input errors
- Add Swagger decorators to endpoints
- Let GlobalExceptionFilter handle responses
- Throw generic Error('message')
- Use console.error instead of logger
- Catch and silently ignore errors
- Expose stack traces in responses
- Return raw error messages to clients
| File | Purpose |
|---|---|
src/common/exceptions/index.ts |
All exception classes |
src/common/exceptions/error-codes.ts |
Error codes and categories |
src/common/filters/global-exception.filter.ts |
Global error handling |
src/common/logging/error-logger.service.ts |
Error logging |
src/common/decorators/swagger-error-responses.decorator.ts |
Swagger docs |
docs/ERROR_HANDLING.md |
Full documentation |
docs/MIGRATION_GUIDE.md |
Migration instructions |
Errors with status code >= 500 are treated as critical:
- Logged with stack trace (dev only)
- Alerted for monitoring
- Graceful shutdown may be triggered
- Full Documentation: See ERROR_HANDLING.md
- Migration Guide: See MIGRATION_GUIDE.md
- Examples: See error-handling.examples.ts
- Tests: See error-handling.e2e-spec.ts
- β Import custom exception
- β Throw with context
- β Let GlobalExceptionFilter catch it
- β Client gets consistent error response
- β Error is logged with full context
- β Swagger documentation is auto-generated
That's it! The rest is handled automatically.