Xử Lý Exception Đúng Cách Thay Vì Try-Catch Tràn Lan

Có một đêm, chúng tôi phải xử lý exception mà đáng ra chỉ cần vài dòng log đã tránh được. Hệ thống backend của một dự án khách hàng treo cứng lúc nửa đêm. Log chỉ ghi vỏn vẹn một dòng: “Object reference not set”. Không ai biết lỗi bắt đầu từ đâu, vì ba tầng try-catch phía trên đã âm thầm nuốt hết exception gốc.

Sau đêm đó, chúng tôi viết lại toàn bộ quy tắc bắt lỗi cho dự án. Bài này là những gì rút ra được, không phải lý thuyết chép lại từ tài liệu.

Những Sai Lầm Thường Gặp Khi Bắt Lỗi Trong Code Backend

Ở nhiều dự án .NET chúng tôi từng vào sửa, try-catch xuất hiện dày đặc. Gần như method nào cũng bọc một khối try-catch, kể cả những chỗ không có gì đáng lo. Nói dễ hiểu, exception (ngoại lệ) là tín hiệu chương trình báo có sự cố — gọi API, đọc file, truy vấn database đều có thể phát sinh lỗi kiểu này. Vấn đề không nằm ở việc bắt lỗi, mà ở chỗ bắt kiểu gì cũng bọc, không phân biệt chỗ nào cần.

Hệ quả là code phình to, luồng chạy chính bị chen ngang bởi hàng chục khối catch rỗng hoặc chỉ log qua loa. Đọc lại sau sáu tháng, chính người viết cũng không nhớ vì sao chỗ đó phải bắt lỗi. Bảo trì trở thành việc mệt mỏi, vì mỗi lần sửa một hàm phải dò qua nhiều lớp catch lồng nhau mới hiểu được luồng thật sự đang chạy.

Tệ hơn cả là kiểu catch (Exception) rồi bỏ trống, hoặc chỉ ghi một dòng log chung chung. Lỗi bị nuốt âm thầm, chương trình vẫn chạy tiếp như chưa có gì xảy ra. Đến khi dữ liệu sai lệch hoặc khách hàng báo lỗi, đội vận hành phải mò lại từ đầu vì log không đủ manh mối. Phần lớn dự án chúng tôi từng hỗ trợ đều thiếu một quy ước xử lý lỗi chung — mỗi người viết theo thói quen riêng, không ai theo chuẩn nào cả.

Một Ca Cụ Thể: Đơn Hàng Biến Mất Không Vì Lý Do Gì

Chúng tôi từng gặp một hệ thống đặt hàng nội bộ, nơi hàm trừ tồn kho được bọc trong một khối try-catch chỉ để “cho chắc”. Khi database tạm nghẽn, lệnh trừ kho ném lỗi timeout. Khối catch đó bắt lỗi, không log gì, rồi hàm vẫn trả về thành công cho tầng gọi phía trên. Kết quả: đơn hàng được ghi nhận, nhưng tồn kho không hề giảm. Một tuần sau, kho lệch số so với hệ thống hàng trăm đơn, và không ai lần ra được nguyên nhân vì log trống trơn.

Sự cố đó dạy chúng tôi một điều đơn giản: catch mà không log, không ném lại, chẳng khác gì việc tắt đèn báo cháy vì tiếng kêu khó chịu. Ngọn lửa không biến mất, chỉ là không ai thấy nữa cho tới khi cháy lan.

Nguyên Tắc Xử Lý Exception Đúng Cách Cho Backend

Từ đêm sự cố đó, chúng tôi rút ra một nguyên tắc gốc: chỉ bắt exception khi biết chính xác sẽ làm gì với nó. Nếu không xử lý được, cứ để nó bay lên tầng trên, nơi có đủ ngữ cảnh để quyết định. Đây là gốc rễ của việc xử lý exception đúng cách, chứ không phải cứ bọc try-catch cho yên tâm.

Chỉ Bắt Lỗi Khi Bạn Thật Sự Xử Lý Được Nó

Nghe đơn giản nhưng dễ làm sai. Ví dụ khi gọi một API bên thứ ba để lấy tỷ giá, nếu API đó lỗi, bạn có thể bắt HttpRequestException rồi thử lại, hoặc trả về giá trị mặc định. Đó là bắt lỗi có mục đích rõ ràng. Ngược lại, nếu một hàm tính toán nội bộ ném lỗi vì dữ liệu null, bắt nó lại rồi chỉ log thôi thì gần như vô nghĩa — lỗi đó cần được sửa tận gốc, không phải che đi bằng một khối catch.

Chúng tôi hay dùng một câu hỏi để lọc: bắt lỗi này xong, chương trình có tiếp tục chạy đúng không? Nếu câu trả lời là không, đừng bắt. Cứ để exception nổ ra ở tầng cao hơn, nơi có middleware xử lý tập trung. Làm vậy code sạch hơn hẳn, vì không còn hàng chục khối catch vô nghĩa nằm rải khắp nơi trong dự án.

Ghi Log Đủ Để Truy Vết, Không Chỉ Để Có Log

Log là thứ cứu chúng tôi trong hầu hết sự cố nửa đêm. Nhưng log kiểu “Error occurred” thì gần như vô dụng. Một log tốt cần ít nhất ba thứ: loại exception, ngữ cảnh lúc lỗi xảy ra (ai gọi, tham số gì), và stack trace gốc chứ không phải bản đã bị bắt lại vài lần. Chúng tôi thường dùng ILogger của ASP.NET Core kèm structured logging, để sau này lọc theo mã đơn hàng hay mã người dùng trong vài giây thay vì đọc cả nghìn dòng text.

Mức độ ghi log càng quan trọng hơn với hệ thống có luồng thanh toán, đặt hàng. Chúng tôi từng hỗ trợ một đội đang vận hành mảng thiết kế website bán hàng cho khách, nơi mỗi exception rớt ở bước checkout đồng nghĩa với một đơn hàng biến mất không dấu vết. Sau khi họ thêm log đủ ngữ cảnh ở lớp xử lý thanh toán, thời gian tìm nguyên nhân mỗi lần lỗi giảm từ nửa ngày xuống còn vài phút.

Phân Loại Mức Độ Nghiêm Trọng Trước Khi Quyết Định Cách Xử Lý

Không phải exception nào cũng đáng báo động lúc 2 giờ sáng. Chúng tôi chia lỗi thành ba nhóm để đội vận hành biết phản ứng đúng mức, thay vì hoảng loạn với mọi cảnh báo:

  • Nghiêm trọng — hệ thống không thể tiếp tục, ví dụ mất kết nối database. Cần cảnh báo ngay và có người trực xử lý.
  • Cảnh báo — một phần chức năng lỗi nhưng hệ thống vẫn chạy được, ví dụ dịch vụ gửi email tạm thời không phản hồi. Ghi log, theo dõi, chưa cần đánh thức ai giữa đêm.
  • Thông tin — lỗi do người dùng nhập sai, không phải lỗi hệ thống. Trả về thông báo rõ ràng là đủ, không cần log ở mức cao.

Phân loại rõ giúp đội ngũ không còn cảm giác “cái gì cũng đỏ”, vốn là lý do khiến nhiều người dần bỏ qua cả những cảnh báo thật sự quan trọng.

Cách Xây Dựng Chiến Lược Xử Lý Lỗi Nhất Quán

Nhiều dự án chúng tôi tiếp nhận có một điểm chung: xử lý lỗi nằm rải khắp controller, service, repository, mỗi nơi viết một kiểu khác nhau. Cách bền hơn là dồn việc bắt exception về một tầng ngoài cùng, thường gọi là exception middleware trong ASP.NET Core. Middleware này chặn mọi lỗi chưa được xử lý ở tầng dưới, ghi log, rồi trả về response chuẩn cho client. Nhờ vậy các tầng bên trong — service, repository — chỉ cần lo nghiệp vụ, không phải viết code bắt lỗi lặp đi lặp lại ở mọi nơi.

Thiết kế này đặc biệt quan trọng khi hệ thống có nhiều API cùng phục vụ một mặt trận, ví dụ vừa có API cho web, vừa có API cho app di động. Nếu bạn đang xây theo hướng nhiều service nhỏ gọi lẫn nhau, phần xử lý lỗi tập trung càng cần rõ ràng ngay từ đầu — chúng tôi từng ghi lại khá kỹ trong bài cách thiết kế API layer để dữ liệu chạy mượt, đáng đọc thêm nếu bạn đang ở giai đoạn đó.

Thông Báo Lỗi Phải Có Ích Cho Cả Người Dùng Lẫn Đội Phát Triển

Một lỗi 500 chung chung không nói lên điều gì. Chúng tôi luôn trả về ba thứ trong response lỗi: mã lỗi nội bộ, thông điệp ngắn gọn cho người dùng, và một traceId để đối chiếu với log phía server. Người dùng cuối không cần thấy stack trace — đó là lỗ hổng bảo mật, không phải sự minh bạch. Nhưng đội phát triển cần traceId đó để tra log trong vài giây, thay vì phải hỏi khách hàng lúc đó họ đang làm gì.

Với lỗi do người dùng nhập sai, thông điệp phải cụ thể: “Số điện thoại không đúng định dạng” tốt hơn nhiều so với “Dữ liệu không hợp lệ”. Khác biệt nhỏ này giảm hẳn số ticket hỗ trợ, vì người dùng tự sửa được ngay, không cần liên hệ ai.

Kiểm Thử Để Chắc Chắn Chiến Lược Thật Sự Hoạt Động

Chiến lược xử lý lỗi chỉ đáng tin khi được kiểm thử, không phải viết xong rồi để đó. Chúng tôi luôn yêu cầu team viết thêm test cho vài tình huống cụ thể: gọi API với dữ liệu thiếu trường bắt buộc để xem response có trả đúng mã lỗi hay không, ngắt kết nối database giả lập để kiểm tra middleware có bắt và log đúng cách, hoặc gửi liên tiếp cùng một request để chắc chắn hệ thống xử lý idempotent — tức là chạy nhiều lần vẫn ra một kết quả, không bị lỗi chồng lỗi.

Một vài đội chúng tôi từng tư vấn đã đi xa hơn, dùng AI agent để mỗi sáng tự quét log lỗi qua đêm và tóm tắt lại điểm bất thường. Nếu bạn tò mò hướng này vận hành ra sao, có thể xem qua ứng dụng AI agent vào workflow tự động cho developer — một góc nhìn khá gần với việc giám sát lỗi hằng ngày.

Kết Luận

Xử lý exception đúng cách không nằm ở việc viết bao nhiêu khối try-catch, mà ở việc biết khi nào nên bắt, khi nào nên để lỗi bay lên, và ghi lại đủ thông tin để lần sau không phải đoán mò. Một hệ thống backend ổn định không phải hệ thống không bao giờ lỗi — mà là hệ thống lỗi một cách có kiểm soát, dễ truy vết, dễ sửa.

Nếu dự án của bạn đang có quá nhiều khối catch rỗng hoặc log chung chung, đừng sửa từng chỗ một. Hãy dừng lại, dựng một lớp xử lý lỗi tập trung trước, rồi dọn dần các khối try-catch thừa. Muốn đi xa hơn nữa, bạn có thể tham khảo thêm các nguyên tắc review code dành cho backend developer — phần lớn lỗi xử lý exception sai thật ra đã có thể bắt được ngay từ bước review, trước khi lên production.