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>
14 KiB
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:
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:
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):
@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):
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):
@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
// BAD ❌
try {
await this.repository.save(entity);
} catch (error) {
// Silent - no logging, no error thrown
}
Fix:
// 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
// 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:
// 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
// BAD ❌
try {
// ...
} catch (error) {
console.error(error); // Not structured, not captured by logging system
throw error;
}
Fix:
// 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
// BAD ❌
try {
// ...
} catch (error) {
this.logger.error(error.message); // Missing stack trace!
throw error;
}
Fix:
// 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
// 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:
// 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
-
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)
-
Add to execute method:
async execute(command: YourCommand): Promise<YourResult> { try { // existing logic } catch (error) { // error handling } } -
Check if domain exception should be re-thrown:
if (error instanceof DomainException) throw error; -
Add appropriate logging:
this.logger.error( `Message: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error.stack : undefined, this.constructor.name ); -
Throw appropriate HTTP exception:
throw new InternalServerErrorException(); // or throw new NotFoundException(); // or throw new BadRequestException(); -
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