Xử lý lỗi là chỗ phân biệt một API nghiệp dư với một API chín chắn. Nếu server trả lỗi là một chuỗi text ("có lỗi xảy ra", "không tìm thấy"), client nhận được không thể làm gì tự động với nó — nó phải đọc chuỗi và đoán: lỗi này có nên retry không? Do input sai hay server sập? Cần báo user hay báo on-call? Chuỗi text là cho người đọc, không phải máy xử lý. gRPC giải quyết bằng một error model có cấu trúc: mỗi lỗi mang một status code chuẩn (từ một bộ cố định) cộng một thông điệp, và tuỳ chọn thêm details có cấu trúc. Bài này (phần 8 loạt gRPC nâng cao) chạy thật để thấy code chuẩn và details giúp client xử lý lỗi tự động thế nào.

Bộ status code chuẩn của gRPC

gRPC không dùng HTTP status code (200, 404, 500). Nó có bộ code riêng — khoảng 17 mã — mỗi mã mang ngữ nghĩa rõ ràng về loại lỗi:

  • InvalidArgument: client gửi dữ liệu sai (id âm, thiếu trường) — lỗi do client, retry vô ích.
  • NotFound: tài nguyên không tồn tại.
  • PermissionDenied / Unauthenticated: không có quyền / chưa xác thực.
  • Unavailable: server tạm không phục vụ được — lỗi tạm thời, nên retry.
  • DeadlineExceeded: hết thời hạn (bài trước).
  • ResourceExhausted: hết quota/rate limit.
  • Internal: lỗi server không mong đợi.

Điểm mấu chốt: code phân loại lỗi theo cách máy hiểu được, nên client quyết định tự động — retry hay không, báo ai, xử lý thế nào — mà không cần đọc text.

// SERVER trả lỗi bằng status code + (tuỳ chọn) details có cấu trúc
if r.Id <= 0 {
    st := status.New(codes.InvalidArgument, "id phai > 0")
    ds, _ := st.WithDetails(&errdetails.BadRequest{
        FieldViolations: []...{{Field:"id", Description:"phai la so duong"}}})
    return nil, ds.Err()
}
if notFound { return nil, status.Error(codes.NotFound, ...) }

// CLIENT đọc code + message + details
st, _ := status.FromError(err)
st.Code()      // codes.InvalidArgument
st.Message()   // "id phai > 0"
for _, d := range st.Details() { /* đọc field violations */ }

Ảnh chụp đoạn mã Go nền tối error model status code chuẩn cộng details có cấu trúc gRPC không dùng HTTP status, server trả lỗi status.New codes.InvalidArgument id phai lớn hơn 0 gắn details WithDetails errdetails.BadRequest FieldViolations Field id Description phai la so duong return ds.Err, if notFound return status.Error codes.NotFound, client đọc code message details status.FromError err st.Code InvalidArgument st.Message st.Details for range đọc field violations, code chuẩn client tự quyết retry Unavailable bỏ InvalidArgument

Hình 1: Server trả lỗi bằng status.Error(code, msg), và có thể gắn details có cấu trúc qua status.New().WithDetails(...) (ví dụ BadRequest liệt kê field nào sai). Client đọc status.FromError() lấy Code(), Message(), và Details() — tất cả máy đọc được, không phải parse chuỗi.

Đo thật: ba tình huống, ba code

Mình dựng server trả các lỗi khác nhau tuỳ input trên go-lab, client đọc và in ra:

Ảnh chụp output thật nền tối code message details client nhận status.FromError server client gRPC Go, id 5 OK, id -1 code InvalidArgument msg id phai lớn hơn 0 details field id desc phai la so duong, id 999 code NotFound msg khong tim thay item 999, client quyết định theo CODE không parse text Unavailable DeadlineExceeded nên retry lỗi tạm thời InvalidArgument NotFound không retry lỗi do input, code bằng ngôn ngữ chung máy hiểu retry alert xử lý tự động details bằng thông tin có cấu trúc field nào sai thay vì một chuỗi text mơ hồ client phải tự đoán

Hình 2: Kết quả thật. id=5 → OK. id=-1 → code InvalidArgument, msg "id phai > 0", kèm details có cấu trúc (field="id", desc="phai la so duong"). id=999 → code NotFound. Client quyết định theo code: Unavailable/DeadlineExceeded nên retry (lỗi tạm thời), InvalidArgument/NotFound không retry (lỗi do input).

Đọc kết quả:

  • id=5 → OK: trường hợp thành công, không lỗi.
  • id=-1 → InvalidArgument + details: server trả code InvalidArgument với message "id phai > 0", và một details có cấu trúc BadRequest chỉ rõ field="id", desc="phai la so duong". Client không chỉ biết "có lỗi input" (qua code) mà còn biết chính xác trường nào sai và vì sao (qua details) — đủ để hiển thị lỗi ngay cạnh ô nhập id trên form, tự động. Đây là điều một chuỗi text không làm được.
  • id=999 → NotFound: code NotFound rõ ràng — client biết đây không phải lỗi hệ thống mà là tài nguyên không tồn tại, xử lý khác hẳn (hiện "không tìm thấy" thay vì "thử lại sau").
  • Client quyết định theo code, không parse text: đây là giá trị cốt lõi. Client viết logic một lần: gặp Unavailable hay DeadlineExceeded → retry (lỗi tạm thời, bài sau); gặp InvalidArgument hay NotFound → không retry (retry vô ích vì input sẽ vẫn sai). Quyết định này dựa trên code — một giá trị enum máy so sánh được — không phải đoán nghĩa từ chuỗi text có thể đổi bất cứ lúc nào.

Thông điệp cốt lõi: code là ngôn ngữ chung giữa server và client để máy xử lý lỗi tự động; details là thông tin có cấu trúc thay cho text mơ hồ. Cùng nhau, chúng biến xử lý lỗi từ "đọc và đoán" thành "phân loại và hành động".

Vì sao không dùng HTTP status cho gRPC

Người quen REST hỏi: sao không dùng 400/404/500? Vì HTTP status quá ít và quá mơ hồ cho RPC. HTTP 400 gộp cả "input sai định dạng", "thiếu trường", "vi phạm ràng buộc nghiệp vụ" vào một mã — client không phân biệt được. HTTP 503 không nói rõ "tạm thời, retry được" hay "quá tải, lùi lại". Bộ code của gRPC mịn hơn và có ngữ nghĩa retry rõ ràng: Unavailable (retry được) tách khỏi FailedPrecondition (đừng retry, phải sửa trạng thái trước) tách khỏi ResourceExhausted (lùi lại rồi retry). Sự phân loại mịn này là cái cho phép retry tự động đúng (bài sau) — thứ HTTP status làm vụng về.

Đánh đổi cần cân nhắc

Chọn đúng code là một kỷ luật, dễ dùng sai. Cám dỗ lớn nhất là trả Internal cho mọi lỗi (vì dễ) — nhưng thế thì client mất khả năng phân biệt lỗi do nó (InvalidArgument) với lỗi do server (Internal), và retry sai. Phải cố ý chọn code đúng ngữ nghĩa cho mỗi lỗi: input sai là InvalidArgument không phải Internal; không quyền là PermissionDenied không phải NotFound. Dùng sai code làm client xử lý sai mà không có lỗi nào báo — giống bẫy "đổi tên field" của schema evolution.

Details tiện nhưng thêm phụ thuộc và phức tạp. errdetails (BadRequest, QuotaFailure, RetryInfo...) là các message protobuf chuẩn của Google, cần import thêm package genproto. Chúng mạnh (ví dụ RetryInfo nói client chờ bao lâu rồi retry), nhưng không phải lúc nào cũng cần — với lỗi đơn giản, code + message là đủ. Thêm details khi client thật sự cần thông tin có cấu trúc (hiển thị lỗi theo field, biết thời gian backoff), không phải mặc định cho mọi lỗi.

Đừng rò rỉ chi tiết nội bộ qua message/details. Message và details đi thẳng tới client — kể cả client không tin cậy. Một lỗi Internal với message chứa nguyên câu SQL hay stack trace là lỗ hổng bảo mật (nhớ bài CLAUDE.md về không trả e.getMessage() của DataIntegrityViolation). Với client ngoài, trả code + message chung chung an toàn; log chi tiết đầy đủ ở server (nơi chỉ bạn đọc), đừng đẩy ra ngoài. Code cho client biết loại lỗi là đủ; chi tiết nhạy cảm ở lại server.

Ba ý mang về

  1. Status code chuẩn cho máy phân loại lỗi, không cần đọc text: đo thật id=-1 → InvalidArgument, id=999 → NotFound, id=5 → OK — client so sánh code (enum) để quyết định, thay vì parse chuỗi text có thể đổi.
  2. Details có cấu trúc hơn hẳn chuỗi mơ hồ: đo thật InvalidArgument kèm BadRequest chỉ rõ field="id" và lý do — client hiển thị lỗi đúng ô nhập tự động, điều một dòng text không làm được.
  3. Code cho retry tự động đúng, nhưng cần kỷ luật: Unavailable/DeadlineExceeded → retry, InvalidArgument/NotFound → không — bộ code mịn hơn HTTP status cho phép quyết định này; phải chọn đúng code (đừng nhét mọi lỗi vào Internal) và không rò rỉ chi tiết nội bộ ra client.

Nguồn

Phần sau ta dùng chính status code này để làm retry tự động: cấu hình retry policy ở client, backoff, và vì sao chỉ retry các code tạm thời — chạy thật đo số lần thử khi server lỗi rồi hồi.