REST error handling leans on HTTP status codes plus whatever shape of error body a team invents. gRPC standardizes this: every RPC either succeeds with OK or fails with one of a fixed, well-defined set of status codes, each with documented semantics for what it means and, critically, whether a client should retry. Getting this mapping right — and putting the right information in the error detail — is most of what separates a gRPC service that's pleasant to consume from one that leaves every caller guessing.
The status code vocabulary
gRPC defines around seventeen status codes, and a handful cover almost every real case: NOT_FOUND for a missing resource, INVALID_ARGUMENT for a malformed request, ALREADY_EXISTS for a conflicting create, PERMISSION_DENIED and UNAUTHENTICATED for authorization failures, FAILED_PRECONDITION for a valid request that can't run in the current state, UNAVAILABLE for a transient infrastructure failure, and DEADLINE_EXCEEDED for a timeout. Picking the wrong one isn't cosmetic — it changes what a well-behaved client does next, since retry logic is usually written against the code, not the message string.
import "google/rpc/error_details.proto";
// Server returns status INVALID_ARGUMENT with a
// google.rpc.BadRequest detail attached:
message BadRequest {
message FieldViolation {
string field = 1;
string description = 2;
}
repeated FieldViolation field_violations = 1;
}
Richer errors with google.rpc.Status
A bare status code and a string message is often not enough context for a client to act on — which field was invalid, what precondition failed. gRPC's standard error model (google.rpc.Status) lets the server attach structured detail messages to an error: a BadRequest listing exactly which fields failed validation, a RetryInfo telling the client how long to back off, or a QuotaFailure explaining which limit was hit. Clients that bother to unpack these details can show a specific, actionable error instead of a generic "something went wrong" — the same value structured API errors provide in REST via RFC 7807 problem details, just standardized at the protocol level instead of invented per team.
Which codes are safe to retry
The single most consequential decision in gRPC error handling is whether a failure is retryable, and the status code is meant to answer that directly. UNAVAILABLE and, with caveats, DEADLINE_EXCEEDED are generally safe to retry (ideally with exponential backoff and jitter) because they indicate a transient condition. INVALID_ARGUMENT, NOT_FOUND, FAILED_PRECONDITION, and PERMISSION_DENIED are not — retrying an invalid request just produces the same invalid request again. gRPC's built-in retry policies (configured per-method in the service config) can automate this, but only if the server picked the correct code in the first place.
Automatic retry on UNAVAILABLE is safe for calls that are idempotent — a GetOrder retried twice does nothing extra. A CreateOrder retried after a genuinely-succeeded-but-timed-out call can create a duplicate. Use idempotency keys for mutating RPCs before turning on automatic retries, exactly as you would for a POST in REST.
Mapping to HTTP at the gateway boundary
When gRPC sits behind a gRPC-Web or REST-to-gRPC gateway (grpc-gateway, Envoy's gRPC-JSON transcoding), status codes need an HTTP equivalent for browser clients — NOT_FOUND becomes 404, INVALID_ARGUMENT becomes 400, UNAVAILABLE becomes 503, and so on. This mapping is fairly standard and handled by the gateway tooling automatically, but it's worth confirming your gateway does it correctly rather than collapsing every gRPC error to a generic 500 — that's a common gap that turns a precise, actionable gRPC error into an opaque one at the edge.
Wrapping up
Good gRPC error handling means picking the status code that accurately describes what went wrong (not just what's convenient), attaching structured detail via google.rpc.Status so clients can act on the failure instead of just logging it, and being deliberate about which codes are safe for automatic retry. Get the code wrong and every downstream retry policy, alert, and gateway mapping built on top of it is wrong too.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.