docs: move 8 audit report files to docs/audits/
Move remaining root-level audit and CQRS handler analysis files to the centralized docs/audits/ directory for consistency. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
545
docs/audits/CQRS_HANDLER_ERROR_HANDLING_GUIDE.md
Normal file
545
docs/audits/CQRS_HANDLER_ERROR_HANDLING_GUIDE.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# CQRS Handler Error Handling Guide
|
||||
## GoodGo Platform Implementation Standards
|
||||
|
||||
---
|
||||
|
||||
## 📌 Quick Reference
|
||||
|
||||
| Status | Count | Modules |
|
||||
|--------|-------|---------|
|
||||
| ✓ Has Error Handling | 11 | auth (5), listings (2), admin, notifications, payments, search |
|
||||
| ✗ Needs Error Handling | 66 | All other handlers + 6 auth handlers |
|
||||
| **Total** | **77** | **All modules** |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Pattern 1: Standard Command Handler with Error Handling
|
||||
|
||||
Use this pattern for **most command handlers**:
|
||||
|
||||
```typescript
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { DomainException, InternalServerErrorException } from '@modules/shared';
|
||||
|
||||
@CommandHandler(YourCommand)
|
||||
export class YourCommandHandler implements ICommandHandler<YourCommand> {
|
||||
private readonly logger = new Logger(this.constructor.name);
|
||||
|
||||
constructor(
|
||||
private readonly repository: IYourRepository,
|
||||
private readonly eventBus: EventBus,
|
||||
) {}
|
||||
|
||||
async execute(command: YourCommand): Promise<YourResult> {
|
||||
try {
|
||||
// Load aggregate
|
||||
const aggregate = await this.repository.findById(command.id);
|
||||
if (!aggregate) {
|
||||
throw new NotFoundException('Aggregate', command.id);
|
||||
}
|
||||
|
||||
// Execute domain logic
|
||||
aggregate.doSomething(command.data);
|
||||
|
||||
// Save state
|
||||
await this.repository.save(aggregate);
|
||||
|
||||
// Publish events
|
||||
const events = aggregate.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
return { id: aggregate.id, status: 'success' };
|
||||
} catch (error) {
|
||||
// Always re-throw domain exceptions
|
||||
if (error instanceof DomainException) throw error;
|
||||
|
||||
// Log unexpected errors
|
||||
this.logger.error(
|
||||
`Command execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
|
||||
// Throw generic HTTP exception
|
||||
throw new InternalServerErrorException('Operation failed, please try again');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Pattern 2: Standard Query Handler with Error Handling
|
||||
|
||||
Use this pattern for **all query handlers**:
|
||||
|
||||
```typescript
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { InternalServerErrorException } from '@modules/shared';
|
||||
|
||||
@QueryHandler(YourQuery)
|
||||
export class YourQueryHandler implements IQueryHandler<YourQuery> {
|
||||
private readonly logger = new Logger(this.constructor.name);
|
||||
|
||||
constructor(private readonly repository: IYourRepository) {}
|
||||
|
||||
async execute(query: YourQuery): Promise<YourQueryResult> {
|
||||
try {
|
||||
const result = await this.repository.find(query.criteria);
|
||||
if (!result) {
|
||||
throw new NotFoundException('Data not found');
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Query execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Unable to fetch data, please try again');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Pattern 3: Bulk Operation with Per-Item Error Handling
|
||||
|
||||
Use this pattern for **batch operations** (like `bulk-moderate-listings`):
|
||||
|
||||
```typescript
|
||||
@CommandHandler(BulkCommand)
|
||||
export class BulkCommandHandler implements ICommandHandler<BulkCommand> {
|
||||
private readonly logger = new Logger(this.constructor.name);
|
||||
|
||||
async execute(command: BulkCommand): Promise<BulkResult> {
|
||||
const succeeded: string[] = [];
|
||||
const failed: Array<{ id: string; reason: string }> = [];
|
||||
|
||||
try {
|
||||
for (const id of command.ids) {
|
||||
try {
|
||||
const item = await this.repository.findById(id);
|
||||
if (!item) {
|
||||
failed.push({ id, reason: 'Item not found' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process item
|
||||
item.update(command.data);
|
||||
await this.repository.save(item);
|
||||
|
||||
succeeded.push(id);
|
||||
} catch (itemError) {
|
||||
const message = itemError instanceof Error ? itemError.message : 'Unknown error';
|
||||
failed.push({ id, reason: message });
|
||||
// Continue processing other items
|
||||
}
|
||||
}
|
||||
|
||||
return { succeeded, failed, processed: command.ids.length };
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Bulk operation failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Batch operation failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Pattern 4: Graceful Degradation (Non-Critical Services)
|
||||
|
||||
Use this pattern when **secondary services can fail without blocking** the main operation (like duplicate detection in `create-listing`):
|
||||
|
||||
```typescript
|
||||
async execute(command: CreateListingCommand): Promise<CreateListingResult> {
|
||||
try {
|
||||
// Critical path - must succeed
|
||||
const listing = await this.createListing(command);
|
||||
await this.repository.save(listing);
|
||||
|
||||
// Non-critical: Duplicate detection
|
||||
let duplicates = [];
|
||||
try {
|
||||
duplicates = await this.duplicateDetector.find({
|
||||
title: command.title,
|
||||
location: command.location,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
'Duplicate detection failed, continuing without warnings',
|
||||
'CreateListingHandler'
|
||||
);
|
||||
// Continue - duplicates are optional
|
||||
}
|
||||
|
||||
// Non-critical: Price validation
|
||||
let priceWarning: PriceWarning | undefined;
|
||||
try {
|
||||
const result = await this.priceValidator.validate(command);
|
||||
if (result.isSuspicious) {
|
||||
priceWarning = result;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
'Price validation failed, continuing without warning',
|
||||
'CreateListingHandler'
|
||||
);
|
||||
// Continue - price warning is optional
|
||||
}
|
||||
|
||||
return {
|
||||
listingId: listing.id,
|
||||
duplicates,
|
||||
priceWarning,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Create listing failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'CreateListingHandler'
|
||||
);
|
||||
throw new InternalServerErrorException('Failed to create listing');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Pattern 5: Authorization/Authentication Errors
|
||||
|
||||
Use this pattern when **authentication can fail** (like `login-user`):
|
||||
|
||||
```typescript
|
||||
@CommandHandler(LoginUserCommand)
|
||||
export class LoginUserHandler implements ICommandHandler<LoginUserCommand> {
|
||||
private readonly logger = new Logger(this.constructor.name);
|
||||
|
||||
constructor(private readonly tokenService: TokenService) {}
|
||||
|
||||
async execute(command: LoginUserCommand): Promise<TokenPair> {
|
||||
try {
|
||||
return await this.tokenService.generateTokenPair({
|
||||
sub: command.userId,
|
||||
phone: command.phone,
|
||||
role: command.role,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Token generation failed for user ${command.userId}: ${error instanceof Error ? error.message : error}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'LoginUserHandler'
|
||||
);
|
||||
// Use specific exception for authentication
|
||||
throw new UnauthorizedException('Unable to create session, please try again');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❌ Common Mistakes to Avoid
|
||||
|
||||
### ❌ Mistake 1: Silent Catch Block
|
||||
```typescript
|
||||
// BAD ❌
|
||||
try {
|
||||
await this.repository.save(entity);
|
||||
} catch (error) {
|
||||
// Silent - no logging, no error thrown
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```typescript
|
||||
// GOOD ✓
|
||||
try {
|
||||
await this.repository.save(entity);
|
||||
} catch (error) {
|
||||
this.logger.error(`Save failed: ${error}`, error?.stack, 'HandlerName');
|
||||
throw new InternalServerErrorException('Save failed');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Mistake 2: Swallowing Domain Exceptions
|
||||
```typescript
|
||||
// BAD ❌
|
||||
try {
|
||||
const result = entity.validate();
|
||||
if (result.isErr) {
|
||||
throw result.unwrapErr(); // ValidationException
|
||||
}
|
||||
// ...
|
||||
} catch (error) {
|
||||
// This swallows the validation error
|
||||
throw new InternalServerErrorException('Failed');
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```typescript
|
||||
// GOOD ✓
|
||||
try {
|
||||
const result = entity.validate();
|
||||
if (result.isErr) {
|
||||
throw result.unwrapErr();
|
||||
}
|
||||
// ...
|
||||
} catch (error) {
|
||||
// Re-throw domain exceptions
|
||||
if (error instanceof DomainException) throw error;
|
||||
|
||||
this.logger.error(`Unexpected: ${error}`, error?.stack);
|
||||
throw new InternalServerErrorException('Failed');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Mistake 3: Logging to Console
|
||||
```typescript
|
||||
// BAD ❌
|
||||
try {
|
||||
// ...
|
||||
} catch (error) {
|
||||
console.error(error); // Not structured, not captured by logging system
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```typescript
|
||||
// GOOD ✓
|
||||
try {
|
||||
// ...
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Operation failed');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Mistake 4: Partial Logging
|
||||
```typescript
|
||||
// BAD ❌
|
||||
try {
|
||||
// ...
|
||||
} catch (error) {
|
||||
this.logger.error(error.message); // Missing stack trace!
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```typescript
|
||||
// GOOD ✓
|
||||
try {
|
||||
// ...
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Mistake 5: Publishing Events Before Confirming Success
|
||||
```typescript
|
||||
// BAD ❌
|
||||
try {
|
||||
aggregate.apply(command);
|
||||
this.eventBus.publish(aggregate.events); // Before save!
|
||||
await this.repository.save(aggregate);
|
||||
} catch (error) {
|
||||
// Event published but entity not saved!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```typescript
|
||||
// GOOD ✓
|
||||
try {
|
||||
aggregate.apply(command);
|
||||
await this.repository.save(aggregate); // Save first
|
||||
|
||||
// Publish events only after successful save
|
||||
const events = aggregate.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
} catch (error) {
|
||||
// No events published - data is consistent
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Audit Checklist for Each Handler
|
||||
|
||||
When reviewing error handling, verify:
|
||||
|
||||
- [ ] **Try-Catch Block**: Wraps entire execute method logic
|
||||
- [ ] **Domain Exception Re-throw**: `if (error instanceof DomainException) throw error;`
|
||||
- [ ] **Error Logging**: Includes message, stack trace, and context
|
||||
- [ ] **Logger Usage**: Uses injected logger, not console
|
||||
- [ ] **Appropriate Exception**: Throws correct HTTP exception type
|
||||
- [ ] **No Silent Catches**: Every catch block has logging or throw
|
||||
- [ ] **Event Publishing**: Only after successful state persistence
|
||||
- [ ] **Transaction Rollback**: Handled by try-catch (implicit or explicit)
|
||||
- [ ] **User Message**: Meaningful error response (not technical details)
|
||||
- [ ] **Logging Context**: Includes handler class name
|
||||
|
||||
---
|
||||
|
||||
## 📋 Implementation Checklist for Handlers Needing Error Handling
|
||||
|
||||
1. **Review existing pattern** in well-implemented handlers:
|
||||
- `auth/commands/login-user` (auth patterns)
|
||||
- `listings/commands/create-listing` (graceful degradation)
|
||||
- `admin/commands/bulk-moderate-listings` (batch operations)
|
||||
|
||||
2. **Add to execute method**:
|
||||
```typescript
|
||||
async execute(command: YourCommand): Promise<YourResult> {
|
||||
try {
|
||||
// existing logic
|
||||
} catch (error) {
|
||||
// error handling
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Check if domain exception should be re-thrown**:
|
||||
```typescript
|
||||
if (error instanceof DomainException) throw error;
|
||||
```
|
||||
|
||||
4. **Add appropriate logging**:
|
||||
```typescript
|
||||
this.logger.error(
|
||||
`Message: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name
|
||||
);
|
||||
```
|
||||
|
||||
5. **Throw appropriate HTTP exception**:
|
||||
```typescript
|
||||
throw new InternalServerErrorException();
|
||||
// or
|
||||
throw new NotFoundException();
|
||||
// or
|
||||
throw new BadRequestException();
|
||||
```
|
||||
|
||||
6. **Test error scenarios**:
|
||||
- Repository returns null/error
|
||||
- Domain validation fails
|
||||
- Database save fails
|
||||
- Event publishing fails
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Priority Implementation Order
|
||||
|
||||
### TIER 1 - Do First (33 handlers)
|
||||
- **admin**: 14 handlers
|
||||
- **leads**: 5 handlers
|
||||
- **inquiries**: 4 handlers
|
||||
- **reviews**: 5 handlers
|
||||
- **subscriptions**: 5 handlers
|
||||
|
||||
**Effort**: ~2 developer-days
|
||||
|
||||
### TIER 2 - Do Second (18 handlers)
|
||||
- **payments**: 4 handlers
|
||||
- **search**: 8 handlers
|
||||
- **listings**: 5 handlers (2 already done)
|
||||
- **agents**: 3 handlers
|
||||
|
||||
**Effort**: ~1 developer-day
|
||||
|
||||
### TIER 3 - Do Last (8 handlers)
|
||||
- **analytics**: 8 handlers
|
||||
- **auth**: 6 handlers (5 already done)
|
||||
|
||||
**Effort**: ~1 developer-day
|
||||
|
||||
---
|
||||
|
||||
## 📞 FAQ
|
||||
|
||||
**Q: Should every handler have error handling?**
|
||||
A: Yes. Every async operation can fail - databases go down, networks fail, etc.
|
||||
|
||||
**Q: Should I catch and swallow domain exceptions?**
|
||||
A: No. Re-throw them using `if (error instanceof DomainException) throw error;`
|
||||
|
||||
**Q: What if the handler has no database calls?**
|
||||
A: Still add error handling. External service calls, calculations, and I/O all need try-catch.
|
||||
|
||||
**Q: Can I use generic Exception handling?**
|
||||
A: No. Import and use NestJS exceptions like `InternalServerErrorException`, `NotFoundException`, etc.
|
||||
|
||||
**Q: Should I log to console?**
|
||||
A: No. Inject and use the NestJS Logger service for structured logging.
|
||||
|
||||
**Q: What about promise rejection handling?**
|
||||
A: Async/await with try-catch handles all promise rejections. That's why we wrap the entire execute method.
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Reference: Exemplary Handlers
|
||||
|
||||
### 1. **Login User Handler** (Excellent)
|
||||
Location: `apps/api/src/modules/auth/application/commands/login-user/login-user.handler.ts`
|
||||
|
||||
✓ Clear error message for user
|
||||
✓ Proper exception type (UnauthorizedException)
|
||||
✓ Stack trace logged
|
||||
✓ Handler context included
|
||||
|
||||
### 2. **Create Listing Handler** (Advanced)
|
||||
Location: `apps/api/src/modules/listings/application/commands/create-listing/create-listing.handler.ts`
|
||||
|
||||
✓ Critical path is protected
|
||||
✓ Non-critical services can degrade gracefully
|
||||
✓ Continues operation even if secondary services fail
|
||||
✓ Warnings still provided when possible
|
||||
|
||||
### 3. **Bulk Moderate Handler** (Batch Pattern)
|
||||
Location: `apps/api/src/modules/admin/application/commands/bulk-moderate-listings/bulk-moderate-listings.handler.ts`
|
||||
|
||||
✓ Per-item error collection
|
||||
✓ Processing continues for other items
|
||||
✓ Complete result returned with failures
|
||||
✓ Individual error tracking
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: April 11, 2026
|
||||
**Audit Coverage**: 77 handlers across 12 modules
|
||||
**Compliance**: 14.3% currently implemented
|
||||
Reference in New Issue
Block a user