# Hướng Dẫn Xử Lý Lỗi cho CQRS Handler ## Tiêu Chuẩn Triển Khai của Nền Tảng GoodGo --- ## 📌 Tham Chiếu Nhanh | Trạng thái | Số lượng | Modules | |--------|-------|---------| | ✓ Đã có Xử Lý Lỗi | 11 | auth (5), listings (2), admin, notifications, payments, search | | ✗ Cần Xử Lý Lỗi | 66 | Tất cả các handler còn lại + 6 auth handler | | **Tổng cộng** | **77** | **Tất cả các module** | --- ## 🎯 Pattern 1: Command Handler Chuẩn với Xử Lý Lỗi Sử dụng pattern này cho **hầu hết các command handler**: ```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 { // Tải aggregate const aggregate = await this.repository.findById(command.id); if (!aggregate) { throw new NotFoundException('Aggregate', command.id); } // Thực thi logic domain aggregate.doSomething(command.data); // Lưu trạng thái await this.repository.save(aggregate); // Phát hành sự kiện const events = aggregate.clearDomainEvents(); for (const event of events) { this.eventBus.publish(event); } return { id: aggregate.id, status: 'success' }; } catch (error) { // Luôn ném lại các domain exception if (error instanceof DomainException) throw error; // Ghi log các lỗi không mong đợi this.logger.error( `Command execution failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error.stack : undefined, this.constructor.name, ); // Ném exception HTTP chung throw new InternalServerErrorException('Operation failed, please try again'); } } } ``` --- ## 🎯 Pattern 2: Query Handler Chuẩn với Xử Lý Lỗi Sử dụng pattern này cho **tất cả các query handler**: ```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: Thao Tác Hàng Loạt với Xử Lý Lỗi Theo Từng Mục Sử dụng pattern này cho **các thao tác batch** (như `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; } // Xử lý từng mục 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 }); // Tiếp tục xử lý các mục khác } } 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: Giảm Cấp Duyên Dáng (Dịch Vụ Không Quan Trọng) Sử dụng pattern này khi **các dịch vụ thứ cấp có thể thất bại mà không chặn** thao tác chính (như phát hiện trùng lặp trong `create-listing`): ```typescript async execute(command: CreateListingCommand): Promise { try { // Luồng quan trọng - phải thành công const listing = await this.createListing(command); await this.repository.save(listing); // Không quan trọng: Phát hiện trùng lặp 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' ); // Tiếp tục - danh sách trùng lặp là tùy chọn } // Không quan trọng: Xác thực giá 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' ); // Tiếp tục - cảnh báo giá là tùy chọn } 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: Lỗi Phân Quyền/Xác Thực Sử dụng pattern này khi **xác thực có thể thất bại** (như `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' ); // Sử dụng exception cụ thể cho xác thực throw new UnauthorizedException('Unable to create session, please try again'); } } } ``` --- ## ❌ Những Lỗi Phổ Biến Cần Tránh ### ❌ Lỗi 1: Khối Catch Im Lặng ```typescript // XẤU ❌ try { await this.repository.save(entity); } catch (error) { // Im lặng - không ghi log, không ném lỗi } ``` **Cách sửa:** ```typescript // TỐT ✓ try { await this.repository.save(entity); } catch (error) { this.logger.error(`Save failed: ${error}`, error?.stack, 'HandlerName'); throw new InternalServerErrorException('Save failed'); } ``` --- ### ❌ Lỗi 2: Nuốt Domain Exception ```typescript // XẤU ❌ try { const result = entity.validate(); if (result.isErr) { throw result.unwrapErr(); // ValidationException } // ... } catch (error) { // Điều này nuốt mất lỗi validation throw new InternalServerErrorException('Failed'); } ``` **Cách sửa:** ```typescript // TỐT ✓ try { const result = entity.validate(); if (result.isErr) { throw result.unwrapErr(); } // ... } catch (error) { // Ném lại các domain exception if (error instanceof DomainException) throw error; this.logger.error(`Unexpected: ${error}`, error?.stack); throw new InternalServerErrorException('Failed'); } ``` --- ### ❌ Lỗi 3: Ghi Log ra Console ```typescript // XẤU ❌ try { // ... } catch (error) { console.error(error); // Không có cấu trúc, không được hệ thống logging ghi nhận throw error; } ``` **Cách sửa:** ```typescript // TỐT ✓ 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'); } ``` --- ### ❌ Lỗi 4: Ghi Log Không Đầy Đủ ```typescript // XẤU ❌ try { // ... } catch (error) { this.logger.error(error.message); // Thiếu stack trace! throw error; } ``` **Cách sửa:** ```typescript // TỐT ✓ try { // ... } catch (error) { this.logger.error( error instanceof Error ? error.message : String(error), error instanceof Error ? error.stack : undefined, this.constructor.name ); throw error; } ``` --- ### ❌ Lỗi 5: Phát Hành Sự Kiện Trước Khi Xác Nhận Thành Công ```typescript // XẤU ❌ try { aggregate.apply(command); this.eventBus.publish(aggregate.events); // Trước khi lưu! await this.repository.save(aggregate); } catch (error) { // Sự kiện đã được phát hành nhưng entity chưa được lưu! } ``` **Cách sửa:** ```typescript // TỐT ✓ try { aggregate.apply(command); await this.repository.save(aggregate); // Lưu trước // Chỉ phát hành sự kiện sau khi lưu thành công const events = aggregate.clearDomainEvents(); for (const event of events) { this.eventBus.publish(event); } } catch (error) { // Không có sự kiện nào được phát hành - dữ liệu nhất quán } ``` --- ## 🔍 Danh Sách Kiểm Tra Khi Kiểm Tra Từng Handler Khi xem xét xử lý lỗi, hãy kiểm tra: - [ ] **Khối Try-Catch**: Bao bọc toàn bộ logic của phương thức execute - [ ] **Ném Lại Domain Exception**: `if (error instanceof DomainException) throw error;` - [ ] **Ghi Log Lỗi**: Bao gồm thông báo, stack trace và ngữ cảnh - [ ] **Sử Dụng Logger**: Dùng logger được inject, không dùng console - [ ] **Exception Phù Hợp**: Ném đúng loại HTTP exception - [ ] **Không Có Catch Im Lặng**: Mỗi khối catch đều có ghi log hoặc ném lỗi - [ ] **Phát Hành Sự Kiện**: Chỉ sau khi lưu trạng thái thành công - [ ] **Rollback Transaction**: Được xử lý bởi try-catch (ngầm định hoặc tường minh) - [ ] **Thông Báo Cho Người Dùng**: Phản hồi lỗi có ý nghĩa (không phải chi tiết kỹ thuật) - [ ] **Ngữ Cảnh Logging**: Bao gồm tên lớp handler --- ## 📋 Danh Sách Triển Khai cho Các Handler Cần Xử Lý Lỗi 1. **Xem lại pattern hiện có** trong các handler được triển khai tốt: - `auth/commands/login-user` (các pattern xác thực) - `listings/commands/create-listing` (giảm cấp duyên dáng) - `admin/commands/bulk-moderate-listings` (các thao tác batch) 2. **Thêm vào phương thức execute**: ```typescript async execute(command: YourCommand): Promise { try { // logic hiện có } catch (error) { // xử lý lỗi } } ``` 3. **Kiểm tra xem domain exception có nên được ném lại không**: ```typescript if (error instanceof DomainException) throw error; ``` 4. **Thêm ghi log phù hợp**: ```typescript this.logger.error( `Message: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error.stack : undefined, this.constructor.name ); ``` 5. **Ném HTTP exception phù hợp**: ```typescript throw new InternalServerErrorException(); // hoặc throw new NotFoundException(); // hoặc throw new BadRequestException(); ``` 6. **Kiểm thử các tình huống lỗi**: - Repository trả về null hoặc lỗi - Xác thực domain thất bại - Lưu cơ sở dữ liệu thất bại - Phát hành sự kiện thất bại --- ## 🚀 Thứ Tự Ưu Tiên Triển Khai ### BẬC 1 - Làm Trước (33 handler) - **admin**: 14 handler - **leads**: 5 handler - **inquiries**: 4 handler - **reviews**: 5 handler - **subscriptions**: 5 handler **Công sức**: ~2 ngày-lập trình viên ### BẬC 2 - Làm Tiếp (18 handler) - **payments**: 4 handler - **search**: 8 handler - **listings**: 5 handler (đã hoàn thành 2) - **agents**: 3 handler **Công sức**: ~1 ngày-lập trình viên ### BẬC 3 - Làm Cuối (8 handler) - **analytics**: 8 handler - **auth**: 6 handler (đã hoàn thành 5) **Công sức**: ~1 ngày-lập trình viên --- ## 📞 Câu Hỏi Thường Gặp **H: Mỗi handler có cần xử lý lỗi không?** Đ: Có. Mọi thao tác bất đồng bộ đều có thể thất bại — cơ sở dữ liệu có thể ngừng hoạt động, mạng có thể lỗi, v.v. **H: Tôi có nên bắt và nuốt domain exception không?** Đ: Không. Hãy ném lại chúng bằng `if (error instanceof DomainException) throw error;` **H: Nếu handler không có lời gọi cơ sở dữ liệu thì sao?** Đ: Vẫn thêm xử lý lỗi. Các lời gọi dịch vụ bên ngoài, phép tính và I/O đều cần try-catch. **H: Tôi có thể dùng xử lý Exception chung không?** Đ: Không. Hãy import và sử dụng các exception của NestJS như `InternalServerErrorException`, `NotFoundException`, v.v. **H: Tôi có nên ghi log ra console không?** Đ: Không. Hãy inject và sử dụng dịch vụ NestJS Logger để ghi log có cấu trúc. **H: Còn việc xử lý promise rejection thì sao?** Đ: Async/await kết hợp với try-catch xử lý tất cả các promise rejection. Đó là lý do tại sao chúng ta bao bọc toàn bộ phương thức execute. --- ## 🎓 Tham Khảo: Các Handler Mẫu ### 1. **Login User Handler** (Xuất sắc) Vị trí: `apps/api/src/modules/auth/application/commands/login-user/login-user.handler.ts` ✓ Thông báo lỗi rõ ràng cho người dùng ✓ Loại exception phù hợp (UnauthorizedException) ✓ Stack trace được ghi log ✓ Ngữ cảnh handler được bao gồm ### 2. **Create Listing Handler** (Nâng cao) Vị trí: `apps/api/src/modules/listings/application/commands/create-listing/create-listing.handler.ts` ✓ Luồng quan trọng được bảo vệ ✓ Các dịch vụ không quan trọng có thể giảm cấp duyên dáng ✓ Tiếp tục hoạt động ngay cả khi các dịch vụ thứ cấp thất bại ✓ Cảnh báo vẫn được cung cấp khi có thể ### 3. **Bulk Moderate Handler** (Pattern Batch) Vị trí: `apps/api/src/modules/admin/application/commands/bulk-moderate-listings/bulk-moderate-listings.handler.ts` ✓ Thu thập lỗi theo từng mục ✓ Tiếp tục xử lý các mục khác ✓ Trả về kết quả đầy đủ kèm danh sách thất bại ✓ Theo dõi lỗi từng mục riêng biệt --- **Cập Nhật Lần Cuối**: 11 tháng 4 năm 2026 **Phạm Vi Kiểm Tra**: 77 handler trên 12 module **Tuân Thủ**: Hiện tại đã triển khai 14,3%