Errors & Status Codes
Errors are part of the contract
Section titled “Errors are part of the contract”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.
The status codes you’ll actually use
Section titled “The status codes you’ll actually use”There are ~17 codes. A handful cover almost everything:
| Code | Meaning | Typical cause |
|---|---|---|
OK | Success | — |
INVALID_ARGUMENT | The request is malformed | Client sent a bad field; fixing the input would help |
NOT_FOUND | The entity doesn’t exist | GetUser on a missing id |
ALREADY_EXISTS | Entity already exists | CreateUser with a taken email |
PERMISSION_DENIED | Authenticated but not allowed | Caller lacks the role |
UNAUTHENTICATED | No valid credentials | Missing or invalid token |
FAILED_PRECONDITION | System state is wrong for this call | Delete a non-empty bucket |
RESOURCE_EXHAUSTED | Quota or rate limit hit | Too many requests |
DEADLINE_EXCEEDED | The call ran past its deadline | Slow server or network |
UNAVAILABLE | Server is down or unreachable | Transient; usually safe to retry |
INTERNAL | A bug on the server | Unexpected failure |
Two distinctions trip people up:
INVALID_ARGUMENTvsFAILED_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.”UNAUTHENTICATEDvsPERMISSION_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.
Returning an error
Section titled “Returning an error”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}import grpc
def GetUser(self, request, context): user = self.store.get(request.id) if user is None: context.abort(grpc.StatusCode.NOT_FOUND, f"user {request.id} not found") return userimport { status } from '@grpc/grpc-js';
getUser(call, callback) { const user = this.store.get(call.request.id); if (!user) { callback({ code: status.NOT_FOUND, message: `user ${call.request.id} not found` }); return; } callback(null, user);}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.
Rich errors with details
Section titled “Rich errors with details”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 }"] 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.