Skip to content

Errors & Status Codes

gRPC does not use HTTP status codes. Every call completes with a gRPC status: a numeric code, an optional human message, and optional structured details. A successful call returns OK; anything else is an error the client receives as a typed status, not a parsed response body.

Choosing the right code is a design decision — clients branch on it, retry on it, and page you on it. Vague codes make an API frustrating; precise ones make it self-explanatory.

There are ~17 codes. A handful cover almost everything:

CodeMeaningTypical cause
OKSuccess
INVALID_ARGUMENTThe request is malformedClient sent a bad field; fixing the input would help
NOT_FOUNDThe entity doesn’t existGetUser on a missing id
ALREADY_EXISTSEntity already existsCreateUser with a taken email
PERMISSION_DENIEDAuthenticated but not allowedCaller lacks the role
UNAUTHENTICATEDNo valid credentialsMissing or invalid token
FAILED_PRECONDITIONSystem state is wrong for this callDelete a non-empty bucket
RESOURCE_EXHAUSTEDQuota or rate limit hitToo many requests
DEADLINE_EXCEEDEDThe call ran past its deadlineSlow server or network
UNAVAILABLEServer is down or unreachableTransient; usually safe to retry
INTERNALA bug on the serverUnexpected failure

Two distinctions trip people up:

  • INVALID_ARGUMENT vs FAILED_PRECONDITION — the first means “your input is wrong, fixing it will help”; the second means “your input is fine, but the system isn’t in a state where this can succeed.”
  • UNAUTHENTICATED vs PERMISSION_DENIED — the first means “I don’t know who you are”; the second means “I know who you are, and you’re not allowed.”

UNAVAILABLE and DEADLINE_EXCEEDED matter for retries: they signal transient failures, so clients (and interceptors) commonly retry them, while INVALID_ARGUMENT should never be retried unchanged.

You don’t return an error message in the response — you complete the call with a status. Every language exposes the same idea:

import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) {
u, ok := s.store[req.Id]
if !ok {
return nil, status.Errorf(codes.NotFound, "user %d not found", req.Id)
}
return u, nil
}

On the client side, the same status comes back as a typed error carrying code and message — so the caller can branch on NOT_FOUND versus UNAVAILABLE without string-matching.

A code and a message are often enough. When they aren’t — when the client needs machine-readable specifics, like which fields failed validation — gRPC has a richer model: google.rpc.Status carries a repeated details list of typed messages.

flowchart LR
  err["gRPC status"] --> code["code: INVALID_ARGUMENT"]
  err --> msg["message: validation failed"]
  err --> det["details[]"]
  det --> bad["BadRequest {
 field: email,
 desc: not a valid address }"]
A gRPC error: code + message, optionally with structured details

The standard detail types (BadRequest, ErrorInfo, QuotaFailure, RetryInfo, and others from google.rpc) let a server say precisely “field email is invalid” or “retry after 3 seconds”, and let clients react programmatically. Use plain codes for the common case; reach for rich details when the client genuinely needs structured error data.

How does a gRPC call report failure?
A client sends a well-formed request but the target bucket is not empty, so the delete cannot proceed. Which code fits best?
What is the difference between UNAUTHENTICATED and PERMISSION_DENIED?
When should you use google.rpc.Status details instead of just a code and message?