Skip to content

gRPC Error Handling Patterns — Status Codes, Rich Details, and Error Propagation

DodaTech Updated 2026-06-28 5 min read

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

  1. Swallowing downstream errors — when service A calls B and B fails, A should forward the error with context, not return a generic INTERNAL error.
  2. Not including error details — a 400 error without field violations forces the client to guess what's wrong. Always include actionable details.
  3. Exposing internal information — error messages should not include stack traces, database queries, or internal IPs. Use ErrorInfo for internal error codes.
  4. Mixing business logic errors with infrastructure errors — distinguish between application errors (invalid input) and system errors (DB down) for proper client handling.
  5. 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

  1. What error detail types does google.rpc provide?
  2. How do you propagate errors across service boundaries without losing information?
  3. Why should clients classify errors into categories?
  4. What is the difference between error details and metadata for error information?
  5. 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 error details should I always include?

Always include ErrorInfo with reason and domain. For validation errors, include BadRequest with field violations. For rate limiting, include RetryInfo and QuotaFailure.

How do I propagate errors in a microservice chain?

Each service wraps the error with its own context: add the service name, correlation ID, and timestamp. Include the original status code so clients can still classify it.

Can I send multiple error details in one response?

Yes. A single gRPC error can contain multiple detail protobuf messages. For example, both BadRequest and RetryInfo in the same INVALID_ARGUMENT error.

How do I handle errors in client-streaming RPCs?

Check the response status after all sends complete. If the server aborts mid-stream, the remaining sends will fail. Handle this in the generator.

What is the maximum size for error details?

Error details share the same 8KB metadata limit. Keep details concise and use ErrorInfo for additional context instead of putting everything in the message.

What's Next

Learn basic gRPC error handling

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro