16 KiB
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:
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 {
// 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:
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: 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):
@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;
}
// 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):
async execute(command: CreateListingCommand): Promise<CreateListingResult> {
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):
@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'
);
// 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
// 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:
// 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
// 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:
// 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
// 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:
// 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 Đủ
// XẤU ❌
try {
// ...
} catch (error) {
this.logger.error(error.message); // Thiếu stack trace!
throw error;
}
Cách sửa:
// 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
// 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:
// 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
-
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)
-
Thêm vào phương thức execute:
async execute(command: YourCommand): Promise<YourResult> { try { // logic hiện có } catch (error) { // xử lý lỗi } } -
Kiểm tra xem domain exception có nên được ném lại không:
if (error instanceof DomainException) throw error; -
Thêm ghi log phù hợp:
this.logger.error( `Message: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error.stack : undefined, this.constructor.name ); -
Ném HTTP exception phù hợp:
throw new InternalServerErrorException(); // hoặc throw new NotFoundException(); // hoặc throw new BadRequestException(); -
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%