16 Error Handling in a gRPC Server
16. Error Handling in a gRPC Server
In modern distributed system architectures, gRPC has become one of the go-to RPC protocols for service-to-service communication thanks to its efficiency, multi-language support, and robust transport (HTTP/2). However, like any distributed system, failures are inevitable—ranging from invalid requests and unavailable services to authentication issues. Good error handling on the gRPC server side has a huge impact on the client experience, the overall reliability of the system, and how easy it is to troubleshoot.
This article covers error-handling practices for gRPC servers based on real-world experience and the best engineering insights, complete with code examples, simulated error scenarios, and a diagram illustrating the flow.
The gRPC Error Model: Reasoning About the Architecture
gRPC builds its error standard on top of status codes from Google RPC and maps them to HTTP statuses. Every RPC can return an error object with:
- A status code (
codes) - An error message
- Optional metadata
The gRPC status codes capture a wide range of common cases, such as:
| Code | Description | HTTP Mapping |
|---|---|---|
| OK | Success | 200 |
| CANCELLED | The client cancelled the request | 499 |
| INVALID_ARGUMENT | Invalid input data | 400 |
| DEADLINE_EXCEEDED | Timeout | 504 |
| NOT_FOUND | Data not found | 404 |
| ALREADY_EXISTS | Resource already exists | 409 |
| PERMISSION_DENIED | No access | 403 |
| UNAUTHENTICATED | Not authenticated | 401 |
| RESOURCE_EXHAUSTED | Quota exhausted | 429 |
| INTERNAL | Server error | 500 |
| UNAVAILABLE | Service unavailable | 503 |
For the full list, check out gRPC Status Codes .
Example Error-Handling Implementation in a gRPC Server (Golang)
We’ll use Go because its gRPC ecosystem is mature and widely adopted by backend engineers.
Suppose we have a UserService with a GetUser RPC whose purpose is to fetch user data by id.
Protobuf definition:
1service UserService {
2 rpc GetUser(GetUserRequest) returns (UserResponse);
3}
4
5message GetUserRequest {
6 string id = 1;
7}
8
9message UserResponse {
10 string id = 1;
11 string name = 2;
12 string email = 3;
13}gRPC Server with Error Handling
1import (
2 "context"
3 "errors"
4
5 pb "github.com/company/proto"
6 "google.golang.org/grpc/codes"
7 "google.golang.org/grpc/status"
8)
9
10type UserServiceServer struct {
11 pb.UnimplementedUserServiceServer
12 users map[string]*pb.UserResponse
13}
14
15func NewUserServiceServer() *UserServiceServer {
16 return &UserServiceServer{
17 users: map[string]*pb.UserResponse{
18 "1": {Id: "1", Name: "Budi", Email: "budi@email.com"},
19 },
20 }
21}
22
23func (s *UserServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.UserResponse, error) {
24 if req.GetId() == "" {
25 // Simulate invalid input
26 return nil, status.Error(codes.InvalidArgument, "user id must not be empty")
27 }
28
29 user, ok := s.users[req.GetId()]
30 if !ok {
31 // Simulate data not found
32 return nil, status.Error(codes.NotFound, "user not found")
33 }
34
35 // Simulate an internal error
36 if req.GetId() == "999" {
37 return nil, status.Errorf(codes.Internal, "internal error: %v", errors.New("simulated panic"))
38 }
39
40 return user, nil
41}Notes:
- Errors are wrapped with
status.Error(codes.XXXX, "error message"). - Try to make error messages clear (not just “error occurred”).
- Map errors according to the root cause—don’t just default to
INTERNAL.
Simulating Error Scenarios
Let’s test 3 error scenarios:
- Invalid input: empty id.
- User not found: the id does not exist in the database.
- Internal error: id = “999”, triggering a system error.
Response Simulation Table
| Request | Error Code | Error Message | HTTP |
|---|---|---|---|
{id: ""} | INVALID_ARGUMENT | user id must not be empty | 400 |
{id: "2"} | NOT_FOUND | user not found | 404 |
{id: "999"} | INTERNAL | internal error: simulated panic | 500 |
Diagram of the gRPC Error-Handling Flow on the Server
Let’s illustrate the error-handling process for a single RPC endpoint:
flowchart TD
A[Client kirim request GetUser] --> B{Validasi Input?}
B -- No --> E[Return INVALID_ARGUMENT]
B -- Yes --> C{User Ada?}
C -- No --> F[Return NOT_FOUND]
C -- Yes --> D{Terjadi Error Internal?}
D -- Yes --> G[Return INTERNAL]
D -- No --> H[Kirim Response Sukses]
How Do You Attach Metadata to an Error?
We often want to send additional metadata with an error—for example, which field failed validation.
gRPC supports the use of an error details extension:
1import "google.golang.org/genproto/googleapis/rpc/errdetails"
2
3// Return details with the error
4badRequest := &errdetails.BadRequest{
5 FieldViolations: []*errdetails.BadRequest_FieldViolation{
6 {
7 Field: "id",
8 Description: "id must not be empty",
9 },
10 },
11}
12
13st := status.New(codes.InvalidArgument, "invalid input")
14st, _ = st.WithDetails(badRequest)
15return nil, st.Err()On the client side, we can parse the details and present a more informative error.
Best Practices for Error Handling in a gRPC Server
- Precise Mapping: Use the correct error code for the context, not just a blanket
INTERNAL. - Human-Readable Messages: Error messages should be clear, non-misleading, and safe from leaking sensitive data.
- Don’t Leak Internal Info: In production, never expose stack traces or DB queries to the client.
- Use Metadata/Details: Add error details when needed for complex validation cases.
- Standardize Handling: Build an error middleware/interceptor so handling is consistent across all endpoints.
- Log Internal Errors: Always log internal errors in detail for investigation without leaking them to the user.
- Graceful Handling: Avoid panics; handle errors within a structured flow.
- Test Error Paths: Don’t just test the happy path—make sure every error scenario is covered.
Closing
Error handling in a gRPC server is not merely about returning an error; it’s about clear communication between systems and engineers. With proper error mapping, clear messages, and metadata extensions on errors, our gRPC system becomes more robust, maintainable, and observable.
Remember, an error that isn’t handled well can be more dangerous than the error itself.
And don’t forget—an error is a form of communication. Let’s make it meaningful!
References:
Thanks for reading! Share your gRPC error-handling experiences or tips in the comments. 🚀