Xử lý lỗi trong code: throw, log hay retry cho đúng

Xử lý lỗi sai là nguyên nhân âm thầm của phần lớn sự cố production: exception bị nuốt, lỗi bị log rồi bỏ qua, hệ thống chạy tiếp với dữ liệu hỏng. Bài này giúp bạn quyết định rõ ràng: khi nào nên ném lỗi lên (throw), khi nào log, khi nào retry, và khi nào thực sự được phép bỏ qua. Bạn sẽ có nguyên tắc áp dụng được cho mọi ngôn ngữ.

Bản chất: lỗi là thông tin, đừng che giấu

Một lỗi cho bạn biết một giả định vừa bị phá vỡ. Câu hỏi cốt lõi không phải “làm sao dập lỗi này đi” mà “ở tầng nào của hệ thống thì tôi có đủ thông tin để quyết định đúng”. Xử lý lỗi ngay tại chỗ phát sinh thường là sai, vì chỗ đó không biết bối cảnh nghiệp vụ.

Lỗi có thể phục hồi và lỗi lập trình

Phân biệt hai loại. Lỗi có thể phục hồi là những thứ dự đoán được: mạng chập chờn, file không tồn tại, input người dùng sai. Chúng cần được xử lý gọn gàng. Lỗi lập trình là bug: null không mong đợi, sai logic. Với loại này, tốt nhất là để nó nổ to và rõ ràng để bạn phát hiện sớm, thay vì che đi.

Quyết định: throw, log, retry hay bỏ qua

Khi nào throw

Khi hàm hiện tại không đủ thông tin để xử lý. Hãy ném lỗi lên cho tầng cao hơn, nơi biết đây là request của ai, giao dịch nào. Kèm theo ngữ cảnh: bọc lỗi gốc trong một lỗi có ý nghĩa nghiệp vụ, đừng để mất stack trace ban đầu.

Khi nào log

Log ở tầng biên, nơi lỗi được xử lý cuối cùng, không phải ở mọi tầng nó đi qua. Log cùng chỗ với nơi bạn quyết định làm gì tiếp. Nếu vừa log vừa ném lên, bạn sẽ có log trùng lặp gây nhiễu.

Khi nào retry

Chỉ retry với lỗi tạm thời và thao tác an toàn khi lặp lại (idempotent). Gọi API bên ngoài bị timeout là ứng viên hợp lý. Nhưng đừng retry một lỗi 400 do input sai — lặp lại vẫn sai. Dùng退避 giãn cách tăng dần (exponential backoff) và giới hạn số lần, nếu không bạn sẽ tự tạo cơn bão request khi dịch vụ kia đang yếu.

Khi nào được bỏ qua

Rất hiếm, và phải cố ý. Ví dụ: ghi log phân tích thất bại thì không nên làm sập luồng chính. Khi bỏ qua, hãy ghi chú rõ lý do trong code, đừng để một khối bắt lỗi trống rỗng.

Ví dụ thực tế

Một dịch vụ thanh toán gọi cổng bên ngoài. Cách sai: bọc toàn bộ trong một khối try, bắt mọi lỗi, log “payment failed” rồi trả về thành công giả. Người dùng nghĩ đã trả tiền, hệ thống nghĩ đã thu, thực tế thì không. Cách đúng: phân loại lỗi. Timeout mạng thì retry có kiểm soát. Cổng từ chối thẻ thì trả lỗi rõ cho người dùng. Lỗi không xác định thì ném lên, đánh dấu giao dịch ở trạng thái cần đối soát, và cảnh báo cho đội vận hành. Mỗi loại lỗi có một đường đi khác nhau.

Lỗi thường gặp và cách sửa

Nuốt exception

Khối bắt lỗi trống hoặc chỉ có một dòng comment là cách nhanh nhất để giấu bug đến tận production. Cách sửa: nếu chưa biết xử lý thế nào, cứ để lỗi ném lên. Đừng bắt chỉ để cho “đỡ đỏ”.

Bắt lỗi quá rộng

Bắt kiểu lỗi chung chung nhất sẽ nuốt luôn cả những lỗi lập trình bạn cần thấy. Cách sửa: bắt đúng loại lỗi cụ thể mà bạn thực sự biết cách xử lý.

Log không có ngữ cảnh

Log chỉ ghi “error occurred” thì vô dụng lúc 3 giờ sáng khi hệ thống sập. Cách sửa: kèm id request, tham số đầu vào liên quan, và giữ nguyên stack trace gốc.

Dùng exception cho luồng bình thường

Ném lỗi để điều khiển logic thông thường làm code khó đọc và có thể chậm. Cách sửa: exception dành cho tình huống ngoại lệ, còn các nhánh dự kiến thì dùng giá trị trả về hoặc kiểu kết quả rõ ràng.

Checklist xử lý lỗi

  • Lỗi này thuộc loại phục hồi được hay là bug? Xử lý khác nhau
  • Tầng hiện tại có đủ thông tin để quyết định không? Nếu không, ném lên
  • Nếu retry: thao tác có idempotent không, có giới hạn số lần và giãn cách không?
  • Mỗi lỗi chỉ được log một lần, tại nơi xử lý cuối, kèm ngữ cảnh
  • Không có khối bắt lỗi trống hay bắt quá rộng
  • Thông báo lỗi cho người dùng khác với chi tiết kỹ thuật ghi vào log
  • Lỗi nghiêm trọng có đường cảnh báo tới người vận hành

Kết luận và bước tiếp theo

Xử lý lỗi tốt là chọn đúng tầng để xử lý và trung thực về những gì đã hỏng. Bước tiếp theo: mở codebase của bạn, tìm các khối bắt lỗi trống hoặc bắt quá rộng, và quyết định lại cho từng chỗ theo bốn hướng throw, log, retry hay bỏ qua có chủ đích.

Câu hỏi thường gặp

Nên trả về mã lỗi hay ném exception?

Tùy ngôn ngữ và tình huống. Với các nhánh dự kiến và thường xuyên, giá trị trả về hoặc kiểu kết quả rõ ràng dễ đọc hơn. Với tình huống thực sự ngoại lệ mà tầng hiện tại không xử lý được, exception phù hợp. Nhiều ngôn ngữ hiện đại khuyến khích kiểu kết quả tường minh cho lỗi dự đoán được.

Retry bao nhiêu lần là hợp lý?

Không có con số vàng, nhưng một cấu hình thực dụng là vài lần với giãn cách tăng dần và tổng thời gian có giới hạn. Quan trọng hơn con số là: chỉ retry lỗi tạm thời, và có điểm dừng để không làm quá tải dịch vụ đang yếu. Mẫu circuit breaker giúp cắt sớm khi phía kia liên tục lỗi.

Có nên bắt lỗi rồi ném lại một lỗi khác không?

Có, đây là kỹ thuật bọc lỗi (wrapping) hữu ích để thêm ngữ cảnh nghiệp vụ. Nhưng phải giữ lại lỗi gốc làm nguyên nhân, đừng vứt bỏ stack trace ban đầu, nếu không bạn mất manh mối debug.

Thông báo lỗi cho người dùng nên chứa gì?

Đủ để họ biết chuyện gì và làm gì tiếp, nhưng không lộ chi tiết kỹ thuật hay dữ liệu nhạy cảm. Chi tiết đầy đủ thuộc về log nội bộ. Kèm một mã lỗi hoặc id để đội hỗ trợ tra cứu là cách làm tốt.

Nguồn tham khảo

  • “Release It!” (Michael Nygard) — mẫu ổn định như circuit breaker, timeout, bulkhead
  • Tài liệu chính thức về xử lý ngoại lệ của ngôn ngữ bạn dùng
  • Google SRE Book — chương về xử lý lỗi và độ tin cậy hệ thống

Similar Posts