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 */ }

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:

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
InvalidArgumentvới message "id phai > 0", và một details có cấu trúcBadRequestchỉ 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ậpidtrên form, tự động. Đây là điều một chuỗi text không làm được. - id=999 → NotFound: code
NotFoundrõ 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
UnavailablehayDeadlineExceeded→ retry (lỗi tạm thời, bài sau); gặpInvalidArgumenthayNotFound→ 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ề
- 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.
- Details có cấu trúc hơn hẳn chuỗi mơ hồ: đo thật InvalidArgument kèm
BadRequestchỉ 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. - 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
- gRPC docs — Error handling & status codes: https://grpc.io/docs/guides/error/
- grpc-go — status package: https://pkg.go.dev/google.golang.org/grpc/status
- Google API — error_details.proto (errdetails): https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto
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.