Bỏ qua để đến nội dung
Knowledge Hub

Xử lý lỗi, comment, trùng lặp và hướng phụ thuộc Trung cấp

Thời gian đọc ước tính: 6 phút

Trang này trả lời: lỗi nào nên là giá trị trả về, lỗi nào là exception; comment gì thì giữ; và khi nào hai đoạn code trùng nhau là quyết định đúng.

Xử lý lỗi: không nuốt, phân loại, thêm context

Phần tiêu đề “Xử lý lỗi: không nuốt, phân loại, thêm context”

Đang tải diagram…

Loại Ví dụ Biểu diễn Ai xử lý
Nhánh nghiệp vụ dự kiến Giỏ rỗng, thẻ bị từ chối, coupon hết hạn Kết quả có kiểu (ok: false) Use case, rồi map sang HTTP
Lỗi hạ tầng Timeout DB, cổng thanh toán 503 Exception kèm context và cause Exception filter, retry, alert
Bug undefined không mong đợi, invariant vỡ Để nổ, đừng bắt Sửa code
export type PlaceOrderResult =
| { ok: true; orderId: string; totalVnd: number }
| { ok: false; error: 'EMPTY_CART' | 'CUSTOMER_NOT_FOUND' | 'PAYMENT_DECLINED' };
export class PaymentUnavailableError extends Error {}
// method private trong CheckoutService
private async charge(cmd: PlaceOrderCommand, totalVnd: number) {
try {
return await this.payments.charge({ amountVnd: totalVnd, idempotencyKey: cmd.idempotencyKey });
} catch (err) {
throw new PaymentUnavailableError(
`charge failed: user=${cmd.userId} amount=${totalVnd}VND key=${cmd.idempotencyKey}`,
{ cause: err },
);
}
}
  • Không nuốt: một catch chỉ hợp lệ khi nó xử lý thật (retry, fallback có chủ đích) hoặc ném lại với thông tin tốt hơn.
  • Thêm context: lỗi gốc ECONNRESET không nói được đơn nào, bao nhiêu tiền. Option cause (ES2022) giữ nguyên lỗi gốc kèm stack; cần lib ES2022 trong tsconfig.
  • Không dùng exception cho nhánh dự kiến: declined xảy ra hàng ngày. Ném exception thì caller phải try/catch để biết kết quả bình thường, và TypeScript không kiểm tra được caller đã xử lý đủ trường hợp chưa.
  • Phân loại tín hiệu từ provider (declined, retryable, unknown outcome) là việc của adapter, xem Adapter pattern.

Ở biên HTTP:

  • Controller map PlaceOrderResult sang status và body theo RFC 9457 Problem Details: CUSTOMER_NOT_FOUND → 404, PAYMENT_DECLINED → 422.
  • PaymentUnavailableError đi qua một NestJS exception filter thành 503, log đủ cause, không lộ chi tiết nội bộ cho client.

Result type cho lỗi dự kiến

  • Chữ ký hàm nói rõ các kết cục; caller quên nhánh thì switch exhaustive báo lỗi compile.
  • Không lẫn “thẻ bị từ chối” với “database chết” trong cùng catch.
  • Test nhánh lỗi chỉ cần so sánh giá trị trả về.

Cái giá

  • Phải truyền kết quả qua từng tầng; lạm dụng cho lỗi hạ tầng sẽ làm code đầy if (!r.ok) return r.
  • Không có cú pháp hỗ trợ như ? của Rust; thư viện Result bên thứ ba là thêm một dependency cả team phải học.
  • Đa số framework (NestJS filter, middleware) xây quanh exception; vẫn cần exception ở rìa.

Comment: giải thích vì sao, xoá code bị comment

Phần tiêu đề “Comment: giải thích vì sao, xoá code bị comment”
Loại comment Giữ? Ví dụ
Kể lại code Xoá // loop qua items cộng total
Giải thích vì sao hoặc ràng buộc bên ngoài Giữ // Cổng TT dedupe theo key trong 24h, nên key phải sinh từ order, không random mỗi lần retry
Cảnh báo hành vi không hiển nhiên Giữ // Giờ theo timezone của process; đổi sang giờ VN là commit riêng
Code bị comment (// await this.mailer...) Xoá Git đã nhớ; để lại chỉ gây câu hỏi “bật lại hay không?”
TODO không có ticket Đổi thành ticket hoặc xoá TODO không chủ thì không bao giờ được làm

Đây là chỗ Martin và Ousterhout bất đồng mạnh nhất: Martin coi comment là “điều bất đắc dĩ”, Ousterhout coi comment là một phần của abstraction. Mặc định thực dụng: tên tốt thay comment “what”; comment “why” thì cứ viết.

Sandi Metz, bài The Wrong Abstraction (2016): “duplication is far cheaper than the wrong abstraction”, và lời khuyên “prefer duplication over the wrong abstraction”.

Đang tải diagram…

  • Lối ra theo Metz: inline abstraction trở lại vào từng caller (Inline Function), mỗi caller xoá phần không dùng, rồi mới tìm abstraction mới cho đúng.
  • Quy tắc ba lần (rule of three, Fowler phổ biến trong Refactoring, ghi nguồn Don Roberts): lần đầu cứ viết, lần hai chấp nhận trùng, lần ba mới refactor. Đến lần ba mới thấy phần nào thực sự chung.
Dấu hiệu Nghiêng về
Hai đoạn giống nhau nhưng đổi vì lý do khác nhau (phí ship B2C vs phí ship B2B) Giữ trùng
Một quy tắc nghiệp vụ duy nhất bị copy (ngưỡng freeship ở 3 chỗ) Gom ngay, đó là một sự thật, không phải ba
Hàm dùng chung có tham số boolean mà mỗi caller truyền một giá trị cố định Abstraction sai, inline lại

Core không import gì từ hạ tầng. Shell phụ thuộc vào interface do phía ứng dụng định nghĩa (PaymentPort, OrderRepository, Clock). NestJS DI cấp implementation qua token, test cấp fake.

export interface Clock { now(): Date }
export const CLOCK = Symbol('CLOCK');
// module
providers: [{ provide: CLOCK, useValue: { now: () => new Date() } satisfies Clock }]
// test
const fixedClock: Clock = { now: () => new Date('2026-10-18T09:00:00+07:00') };
  • Chi tiết token, useClass/useValue và lỗi resolve: NestJS Dependency Injection.
  • Kiểm tra nhanh: muốn test một hàm mà phải mock module fs, Date hay fetch toàn cục, thì phụ thuộc đang đi sai hướng.

Bốn smell Long Function, Long Parameter List, Primitive Obsession, Repeated Switches là tên trong chương 3 sách Refactoring (bản 2) của Fowler. Các dòng còn lại là mô tả của cụm này, không phải tên smell trong sách. Tên refactoring khớp catalog.

Smell Trong ví dụ Refactoring
Long Function checkout 30 dòng, 4 tầng Extract Function, Split Phase
Long Parameter List 5 tham số rời Introduce Parameter Object
Flag argument express, dryRun Remove Flag Argument
Nested conditionals if lồng Replace Nested Conditional with Guard Clauses
Magic literal 500000, 0.9 Replace Magic Literal
Primitive Obsession Tiền là number trần, coupon là string Replace Primitive with Object
Repeated Switches switch (shipping) ở nhiều file Replace Conditional with Polymorphism (Strategy)
Dead code / code bị comment // await this.mailer... Remove Dead Code
Query lẫn side effect Tính tiền và ghi DB cùng hàm Separate Query from Modifier
Abstraction sai Hàm chung đầy flag Inline Function, rồi Extract lại

Tự kiểm tra

  1. “Thẻ bị từ chối” và “cổng thanh toán timeout” nên được biểu diễn khác nhau thế nào, và vì sao?
  2. Khi nào giữ hai đoạn code trùng nhau là quyết định đúng?

Tiếp theo: Lỗi hay gặp