Clean Code
07 Phần III · Cấu trúc & Độ bền Chương 7

Xử lý lỗi

Error Handling

Xử lý lỗi là việc cần thiết, nhưng nếu nó che lấp logic chính thì code đã sai. Cả chương xoay quanh một ý: hãy coi xử lý lỗi là một mối quan tâm tách biệt (separate concern) — suy luận độc lập với logic nghiệp vụ. Code sạch vừa dễ đọc vừa bền (robust); hai điều đó không mâu thuẫn.

01 Dùng exception thay mã lỗi

Cách cũ là trả về cờ lỗi (error flag) hay mã lỗi (return code) để người gọi kiểm tra ngay. Hệ quả: code người gọi rối tung vì kiểm lỗi xen lẫn logic, và rất dễ quên kiểm. Dùng exception tách thuật toán chính khỏi việc xử lý lỗi.

Ví dụ kinh điển DeviceController.sendShutDown(): bản đầu dùng if lồng nhau kiểm handle/status; bản exception gọi tryToShutDown() (lo logic tắt) và bắt DeviceShutDownError ở ngoài.

Python · mã lỗi✗ Bẩn
def send_shut_down(self):
    handle = get_handle(DEV1)          # if lồng theo mã lỗi
    if handle != INVALID:
        record = retrieve_record(handle)
        if record.get_status() != SUSPENDED:
            pause_device(handle)
            clear_status(handle)
        else:
            logger.log("Device suspended, can't shut down")
    else:
        logger.log("Invalid handle")  # lỗi xen lẫn logic chính
Python · exception✓ Sạch
def send_shut_down(self):
    try:
        self.try_to_shut_down()       # logic tắt thiết bị
    except DeviceShutDownError as e:
        logger.log(e)                 # xử lý lỗi tách riêng

def try_to_shut_down(self):
    handle = self.get_handle(DEV1)    # get_handle ném lỗi nếu hỏng
    record = self.retrieve_record(handle)
    self.pause_device(handle); self.clear_status(handle)

Vì sao: hai mối quan tâm — thuật toán tắt thiết bịxử lý lỗi — được tách ra, mỗi cái hiểu được độc lập. Người gọi không thể "quên" kiểm lỗi như với mã trả về.

02 Viết câu lệnh try-catch-finally trước

Exception định nghĩa một phạm vi (scope) trong chương trình. Khối try giống như một transaction: khối catch phải để chương trình ở trạng thái nhất quán dù bên trong try có chuyện gì xảy ra. Vì thế, khi viết code có thể ném lỗi, hãy bắt đầu bằng try-catch-finally.

Kỹ thuật theo TDD: viết một unit test ép ném exception trước (vd test mong đợi StorageException khi file không tồn tại) → tạo stub cho qua → thêm try-catch chuyển FileNotFound thành StorageException → rồi dùng TDD bồi dần logic vào giữa phần tạo stream và phần đóng stream, coi như không có gì sai.

Python · để ngỏ lỗi✗ Bẩn
def load_index(path):
    f = open(path, "rb")          # FileNotFound rò ra ngoài
    data = f.read()               # nếu hỏng giữa chừng → file không đóng,
    f.close()                     # trạng thái không nhất quán
    return deserialize(data)
Python · scope trước, logic sau✓ Sạch
def load_index(path):
    try:
        with open(path, "rb") as f:   # try định nghĩa transaction
            return deserialize(f.read())
    except FileNotFoundError as e:
        raise StorageException("index không tồn tại") from e
    # logic được bồi vào GIỮA, scope lỗi dựng sẵn ngay từ đầu

Vì sao: dựng try trước buộc bạn nghĩ ngay: "nếu hỏng, làm sao để trạng thái vẫn nhất quán?". Logic chính lấp vào giữa một khung đã an toàn, thay vì chắp vá xử lý lỗi về sau.

03 Ưu tiên Unchecked Exception

Checked exception (kiểu Java buộc khai báo throws) nghe hấp dẫn nhưng có cái giá: nó vi phạm Nguyên lý Đóng–Mở (Open/Closed Principle).

Cascade chữ ký

signature cascade

Một hàm cấp thấp ném checked exception mớimọi hàm trên đường tới chỗ catch phải sửa mệnh đề throws. Thay đổi lan từ thấp lên cao, phải build & deploy lại dù chúng không quan tâm lỗi đó.

Phá đóng gói

encapsulation broken

Mọi hàm trên đường truyền buộc phải biết chi tiết lỗi cấp thấp nhất — chi tiết cấp thấp rò lên tận cấp cao.

Khi nào checked vẫn đáng: với thư viện cực kỳ quan trọng mà việc bắt mọi exception là bắt buộc, checked có ích. Nhưng với ứng dụng thông thường, chi phí phụ thuộc thường lớn hơn lợi ích — nên ưu tiên unchecked.

04 Cung cấp ngữ cảnh kèm exception

Mỗi exception nên mang đủ ngữ cảnh để xác định nguồnvị trí lỗi. Stack trace cho biết vị trí, nhưng không nói lên ý định (intent) của thao tác đã thất bại. Hãy tạo thông điệp lỗi có thông tin: nêu rõ thao tác bị lỗi và loại thất bại, rồi truyền đủ thông tin để ghi log trong khối catch.

RAG  Trong pipeline RAG, một except trống rỗng khiến lúc retrieve hỏng bạn chỉ thấy stack trace mơ hồ. Hãy bọc kèm thao tác & tham số:

Python · thiếu ngữ cảnh✗ Bẩn
def retrieve(query):
    try:
        return store.search(embed(query), k=5)
    except Exception:
        raise RetrievalError()        # không biết lỗi gì, query nào
Python · nêu thao tác + loại thất bại✓ Sạch
def retrieve(query):
    try:
        return store.search(embed(query), k=5)
    except VectorStoreTimeout as e:   # nêu rõ thao tác & nguyên nhân
        raise RetrievalError(
            f"truy hồi thất bại (timeout) cho query={query!r}, k=5"
        ) from e                      # đủ info để log trong catch

Vì sao: thông điệp nêu thao tác ("truy hồi") + loại thất bại ("timeout") + tham số, nên khi đọc log ta hiểu ngay ý định nào đã hỏng — điều stack trace một mình không cho biết.

05 Định nghĩa exception theo nhu cầu người gọi

Cách phân loại exception quan trọng nhất là theo cách chúng được bắt (how they are caught). Ví dụ tệ: gọi API bên thứ ba ACMEPort.open() rồi catch riêng từng loại — DeviceResponseException, ATM1212UnlockedException, GMXError — mà mỗi catch làm gần như cùng một việc (log + report).

Python · bắt từng loại của API✗ Bẩn
try:
    port.open()
except DeviceResponseException as e:
    report_port_error(e); log("Device response")     # trùng lặp
except ATM1212UnlockedException as e:
    report_port_error(e); log("Unlock failure")      # phụ thuộc chặt
except GMXError as e:
    report_port_error(e); log("Device failure")      # vào API ACME
Python · bọc API, ném 1 kiểu chung✓ Sạch
class LocalPort:                       # wrapper bọc ACMEPort
    def __init__(self, port): self._inner = ACMEPort(port)
    def open(self):
        try:
            self._inner.open()
        except (DeviceResponseException,
                ATM1212UnlockedException, GMXError) as e:
            raise PortDeviceFailure(e) from e   # 1 kiểu chung

# nơi dùng chỉ còn 1 catch:
try: port.open()
except PortDeviceFailure as e: report_port_error(e); log(e)

Vì sao — bọc third-party API là best practice: LocalPort nuốt 3 loại lỗi của ACME và ném PortDeviceFailure chung, nên nơi dùng chỉ cần một catch. Bọc API giảm phụ thuộc, dễ đổi thư viện sau này, và dễ mock khi test.

06 Định nghĩa luồng bình thường · Special Case Pattern

Dùng exception cho logic nghiệp vụ thông thường sẽ làm rối thuật toán chính. Ví dụ tính chi phí ăn: nếu nhân viên có khai báo bữa ăn thì cộng, nếu không thì cộng mức khoán (per diem) — viết bằng try/catch khiến special case xen vào luồng chính.

Special Case Pattern (Fowler): đổi đối tượng/DAO để nó luôn trả về một object hợp lệ; trường hợp đặc biệt được đóng gói bên trong object đó.

Python · special case lộ ra✗ Bẩn
try:
    expenses = dao.get_meals(employee_id)
    total += expenses.get_total()
except MealExpensesNotFound:
    total += get_meal_per_diem()      # luồng đặc biệt xen vào logic chính
Python · đóng gói special case✓ Sạch
class PerDiemMealExpenses:            # Special Case object
    def get_total(self):
        return MEAL_PER_DIEM          # trả mức khoán

def get_meals(self, employee_id):    # DAO LUÔN trả về một MealExpenses
    return self._find(employee_id) or PerDiemMealExpenses()

# nơi gọi sạch bong, không còn try/catch:
expenses = dao.get_meals(employee_id)
total += expenses.get_total()

Vì sao: client không phải xử lý trường hợp đặc biệt nữa — nó đã được đóng gói trong PerDiemMealExpenses. Luồng bình thường (normal flow) trở thành luồng duy nhất; code gọi chỉ còn hai dòng thẳng băng.

07 Đừng trả null · Đừng truyền null

Trả về null đẩy gánh nặng cho người gọi: phải kiểm nullkhắp nơi; chỉ thiếu một lần kiểm là NullPointerException và mất kiểm soát. Thay vì trả null → ném exception hoặc trả về Special Case object. Với danh sách, trả về danh sách rỗng (Collections.emptyList()) để vòng lặp không cần kiểm null.

Python · trả null✗ Bẩn
def get_chunks(doc_id):
    if not exists(doc_id):
        return None                   # buộc mọi nơi gọi phải kiểm null

chunks = get_chunks(doc_id)
if chunks is not None:                # quên một lần → NPE
    for c in chunks:
        index(c)
Python · danh sách rỗng✓ Sạch
def get_chunks(doc_id):
    if not exists(doc_id):
        return []                     # empty list thay null

for c in get_chunks(doc_id):          # không cần kiểm null
    index(c)

Vì sao: trả danh sách rỗng khiến vòng for chạy 0 lần một cách tự nhiên — bỏ hẳn nhánh kiểm null dễ quên, code gọn và bền hơn.

Truyền null vào hàm còn tệ hơn trả null: rất dễ gây lỗi runtime. Ví dụ x_projection(p1, p2) — truyền None sẽ gây NPE. Hầu hết ngôn ngữ không có cách tốt để xử lý null bị truyền nhầm, nên hợp lý nhất là cấm truyền null theo mặc định.

Python · nhận null âm thầm✗ Bẩn
def x_projection(p1, p2):
    return (p1.x + p2.x) * 1.5        # p1/p2 = None → NPE bí ẩn,
                                      # lỗi nổ sâu trong hàm
Python · cấm null từ cửa✓ Sạch
def x_projection(p1, p2):
    if p1 is None or p2 is None:
        raise InvalidArgumentException("p1, p2 không được null")
    return (p1.x + p2.x) * 1.5        # mặc định: không chấp nhận null

Vì sao: cấm null theo mặc định khiến lỗi lộ ngay tại cửa với thông điệp rõ ràng, thay vì nổ NPE mơ hồ sâu bên trong. Ít sai sót hơn, code robust hơn.

08 Ghi nhớ nhanh

Exception thay mã lỗi. Tách thuật toán chính khỏi xử lý lỗi; người gọi không thể "quên" kiểm.

Viết try-catch-finally trước. try như transaction — giữ trạng thái nhất quán; bồi logic vào giữa.

Ưu tiên unchecked. Checked vi phạm OCP & phá đóng gói (cascade chữ ký throws).

Cung cấp ngữ cảnh. Thông điệp nêu thao tác + loại thất bại; stack trace không nói intent.

Định nghĩa theo người gọi. Bọc third-party API, ném một kiểu chung → một catch.

Special Case Pattern. Trả object luôn hợp lệ, đóng gói trường hợp đặc biệt — bỏ try/catch khỏi luồng chính.

Đừng trả / đừng truyền null. Dùng exception, Special Case, hoặc danh sách rỗng; cấm null theo mặc định.

NguồnChương 7 (Error Handling), Clean Code: A Handbook of Agile Software Craftsmanship — Robert C. Martin, Prentice Hall 2008.