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:
Ho Ngoc Hai
2026-04-11 19:15:24 +07:00
parent 80725ed81f
commit 514aa507db
8 changed files with 3673 additions and 398 deletions

View 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