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
Cần đọc trước
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 CheckoutServiceprivate 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
catchchỉ 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
ECONNRESETkhông nói được đơn nào, bao nhiêu tiền. Optioncause(ES2022) giữ nguyên lỗi gốc kèm stack; cầnlibES2022 trongtsconfig. - 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
PlaceOrderResultsang 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ì
switchexhaustive 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.
Trùng lặp vs abstraction sai
Phần tiêu đề “Trùng lặp vs abstraction sai”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 |
Hướng phụ thuộc và khả năng test
Phần tiêu đề “Hướng phụ thuộc và khả năng test”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');
// moduleproviders: [{ provide: CLOCK, useValue: { now: () => new Date() } satisfies Clock }]
// testconst fixedClock: Clock = { now: () => new Date('2026-10-18T09:00:00+07:00') };- Chi tiết token,
useClass/useValuevà lỗi resolve: NestJS Dependency Injection. - Kiểm tra nhanh: muốn test một hàm mà phải mock module
fs,Datehayfetchtoàn cục, thì phụ thuộc đang đi sai hướng.
Bảng code smell → refactoring
Phần tiêu đề “Bảng code smell → refactoring”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
- “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?
- Khi nào giữ hai đoạn code trùng nhau là quyết định đúng?
Tiếp theo: Lỗi hay gặp