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.
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
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ị và 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.
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)
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ý
Một hàm cấp thấp ném checked exception mới → mọ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
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ồn và vị 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ố:
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
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).
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
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 đó.
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
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 null ở khắ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.
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)
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.
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
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.