# 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 { private readonly logger = new Logger(this.constructor.name); constructor( private readonly repository: IYourRepository, private readonly eventBus: EventBus, ) {} async execute(command: YourCommand): Promise { 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 { private readonly logger = new Logger(this.constructor.name); constructor(private readonly repository: IYourRepository) {} async execute(query: YourQuery): Promise { 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 { private readonly logger = new Logger(this.constructor.name); async execute(command: BulkCommand): Promise { 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 { 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 { private readonly logger = new Logger(this.constructor.name); constructor(private readonly tokenService: TokenService) {} async execute(command: LoginUserCommand): Promise { 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 { 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