Files
goodgo-platform/docs/audits/CQRS_HANDLER_ERROR_HANDLING_GUIDE.md
Ho Ngoc Hai 11f2bf26e6
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 29s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m42s
Deploy / Build Web Image (push) Failing after 27s
Deploy / Build AI Services Image (push) Failing after 29s
E2E Tests / Playwright E2E (push) Failing after 43s
Deploy / Build API Image (push) Failing after 1m31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 6s
Security Scanning / Trivy Scan — API Image (push) Failing after 5m35s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 3m45s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Scan — Web Image (push) Failing after 13m51s
Security Scanning / Trivy Filesystem Scan (push) Failing after 14m46s
Security Scanning / Security Gate (push) Has been cancelled
chore: update project documentation, audit reports, and initialize IDE configuration files
2026-04-19 03:12:54 +07:00

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

  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:

    async execute(command: YourCommand): Promise<YourResult> {
      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:

    if (error instanceof DomainException) throw error;
    
  4. 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
    );
    
  5. Ném HTTP exception phù hợp:

    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%