gRPC Error Handling Patterns — Status Codes, Rich Details, and Error Propagation
In this tutorial, you will learn about grpc error handling patterns. We cover key concepts, practical examples, and best practices to help you master this topic.
gRPC error handling patterns use structured status codes combined with rich error details from google.rpc to communicate precise error information between services, enabling clients to handle failures programmatically.
What You'll Learn
- Using google.rpc.Status for rich error details
- Error propagation across service boundaries
- Client-side error classification and routing
- Structured error logging and monitoring
- Error handling in streaming RPCs
- Error wrapping and middleware patterns
Why It Matters
A simple error code and message are often not enough. Clients need to know which field was invalid, how long to wait before retrying, or which quota was exceeded. Rich error details provide this structure. DodaTech's Durga Antivirus Pro uses rich error details across 50+ Microservices, enabling clients to automatically handle rate limits, retries, and validation errors without human intervention.
Real-World Use
A client sends a threat report with an invalid device ID and exceeds the rate limit. The server returns a single error with both BadRequest field violations and RetryInfo details. The client extracts both, shows the user which field is wrong, and retries after the suggested delay.
flowchart TB
A["Server Error"] --> B["Status Code\n(e.g., INVALID_ARGUMENT)"]
A --> C["Error Message"]
A --> D["Error Details\n(google.rpc)"]
D --> E["BadRequest\n(field violations)"]
D --> F["RetryInfo\n(retry delay)"]
D --> G["QuotaFailure\n(limit exceeded)"]
D --> H["ErrorInfo\n(reason + domain)"]
E --> I["Client: Show validation error"]
F --> J["Client: Retry after delay"]
G --> K["Client: Show quota info"]
H --> L["Client: Log & report"]
Code Examples
Example 1: Rich Error Details in Go
package main
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)
func (s *server) ReportThreat(ctx context.Context,
req *pb.ThreatRequest) (*pb.ThreatResponse, error) {
var violations []*errdetails.BadRequest_FieldViolation
if req.DeviceId == "" {
violations = append(violations, &errdetails.BadRequest_FieldViolation{
Field: "device_id",
Description: "Device ID must not be empty",
})
}
if req.ThreatName == "" {
violations = append(violations, &errdetails.BadRequest_FieldViolation{
Field: "threat_name",
Description: "Threat name must not be empty",
})
}
if len(violations) > 0 {
st := status.New(codes.InvalidArgument,
"Validation failed")
badRequest := &errdetails.BadRequest{}
badRequest.FieldViolations = violations
st, _ = st.WithDetails(badRequest)
// Add retry info for client
retryInfo := &errdetails.RetryInfo{
RetryDelay: &durationpb.Duration{
Seconds: 30,
},
}
st, _ = st.WithDetails(retryInfo)
return nil, st.Err()
}
// Process threat...
return &pb.ThreatResponse{Status: pb.ThreatStatus_QUARANTINED}, nil
}
Example 2: Client Error Classification in Python
import grpc
from google.rpc import error_details_pb2
from google.rpc import code_pb2
class ClassifiedError(Exception):
def __init__(self, code, message, details=None):
self.code = code
self.message = message
self.details = details or {}
def classify_grpc_error(error):
"""Convert gRPC error to classified exception."""
code = error.code()
details = {}
for detail in error.details():
if isinstance(detail, error_details_pb2.BadRequest):
details["field_errors"] = [
{"field": v.field, "desc": v.description}
for v in detail.field_violations
]
elif isinstance(detail, error_details_pb2.RetryInfo):
details["retry_delay"] = detail.retry_delay.seconds
elif isinstance(detail, error_details_pb2.QuotaFailure):
details["quota"] = [
{"metric": v.subject, "limit": v.description}
for v in detail.violations
]
elif isinstance(detail, error_details_pb2.ErrorInfo):
details["reason"] = detail.reason
details["domain"] = detail.domain
if code == grpc.StatusCode.INVALID_ARGUMENT:
return ClassifiedError("VALIDATION_ERROR", error.details(), details)
elif code == grpc.StatusCode.UNAVAILABLE:
return ClassifiedError("NETWORK_ERROR", "Service unavailable", details)
elif code == grpc.StatusCode.RESOURCE_EXHAUSTED:
return ClassifiedError("RATE_LIMITED", "Rate limit exceeded", details)
elif code == grpc.StatusCode.PERMISSION_DENIED:
return ClassifiedError("AUTH_ERROR", "Permission denied")
return ClassifiedError("UNKNOWN", error.details())
# Usage
try:
response = client.ReportThreat(request)
except grpc.RpcError as e:
classified = classify_grpc_error(e)
if classified.code == "VALIDATION_ERROR":
for field_error in classified.details.get("field_errors", []):
print(f"Field {field_error['field']}: {field_error['desc']}")
elif classified.code == "RATE_LIMITED":
delay = classified.details.get("retry_delay", 30)
print(f"Rate limited, retry in {delay}s")
elif classified.code == "NETWORK_ERROR":
print("Service unavailable, will retry...")
elif classified.code == "AUTH_ERROR":
redirect_to_login()
Example 3: Error Propagation in Node.js
const grpc = require('@grpc/grpc-js');
const { StatusBuilder } = require('@grpc/grpc-js');
class ErrorPropagator {
// Wrap and forward error from downstream service
async forwardError(error, context) {
const metadata = new grpc.Metadata();
// Add context about the current service
metadata.set('x-service', 'threat-processor');
metadata.set('x-correlation-id', context.correlationId);
// Forward the original error info
if (error.metadata) {
const originalService = error.metadata.get('x-service')[0];
if (originalService) {
metadata.set('x-original-service', originalService);
}
}
return {
code: error.code || grpc.status.INTERNAL,
details: `[threat-processor] ${error.details}`,
metadata,
};
}
// Handle the error in client
handleError(error) {
const service = error.metadata?.get('x-service')[0] || 'unknown';
const correlationId = error.metadata?.get('x-correlation-id')[0] || 'none';
console.error(`Error from ${service}: ${error.details}`);
console.error(`Correlation ID: ${correlationId}`);
return this.classify(error);
}
}
const propagator = new ErrorPropagator();
Common Mistakes
- Swallowing downstream errors — when service A calls B and B fails, A should forward the error with context, not return a generic INTERNAL error.
- Not including error details — a 400 error without field violations forces the client to guess what's wrong. Always include actionable details.
- Exposing internal information — error messages should not include stack traces, database queries, or internal IPs. Use ErrorInfo for internal error codes.
- Mixing business logic errors with infrastructure errors — distinguish between application errors (invalid input) and system errors (DB down) for proper client handling.
- Not handling errors in streaming — streaming RPCs can fail mid-stream. Handle errors in the stream's error event and clean up resources.
Practice Questions
- What error detail types does google.rpc provide?
- How do you propagate errors across service boundaries without losing information?
- Why should clients classify errors into categories?
- What is the difference between error details and metadata for error information?
- How do you handle errors in server-streaming RPCs?
Challenge: Design an error handling system for a 10-service microservice architecture where each service adds its own context to errors before propagating them, and clients can trace the exact path of a failure across all services.
Mini Project
Build a comprehensive error handling library for gRPC services with: rich error details (BadRequest, RetryInfo, QuotaFailure, ErrorInfo), automatic error propagation across service boundaries, client-side error classification, structured error logging with correlation IDs, and monitoring metrics for error rates by type.
FAQ
What's Next
Learn basic gRPC error handling
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro