gRPC Cheatsheet
Error Handling
Use this gRPC reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Error Model
gRPC errors consist of:
- Status code — a numeric code from the grpc.status enum
- Status message — a short human-readable string
- Details — optional extended description string (Node.js)
- Trailing metadata — optional grpc.Metadata attached to the error
// Error object shape received by clients { code: grpc.status.NOT_FOUND, // number (5) message: 'user not found', details: 'no row matched id=42', // optional metadata: grpc.Metadata, // optional trailing metadata }
Status Codes Reference
const grpc = require('@grpc/grpc-js'); grpc.status.OK; // 0 — success grpc.status.CANCELLED; // 1 — operation cancelled by caller grpc.status.UNKNOWN; // 2 — unknown error (catch-all) grpc.status.INVALID_ARGUMENT; // 3 — bad request input grpc.status.DEADLINE_EXCEEDED; // 4 — deadline passed before completion grpc.status.NOT_FOUND; // 5 — entity does not exist grpc.status.ALREADY_EXISTS; // 6 — creation conflicts with existing record grpc.status.PERMISSION_DENIED; // 7 — authenticated but not authorized grpc.status.RESOURCE_EXHAUSTED; // 8 — quota/rate limit hit grpc.status.FAILED_PRECONDITION; // 9 — system state prevents operation grpc.status.ABORTED; // 10 — concurrency conflict (optimistic lock) grpc.status.OUT_OF_RANGE; // 11 — value out of valid range grpc.status.UNIMPLEMENTED; // 12 — method not implemented on server grpc.status.INTERNAL; // 13 — unexpected server-side error grpc.status.UNAVAILABLE; // 14 — server unreachable — safe to retry grpc.status.DATA_LOSS; // 15 — unrecoverable data corruption grpc.status.UNAUTHENTICATED; // 16 — missing or invalid credentials
When to Use Which Code
| Situation | Code |
|---|---|
| Bad field value, missing required field | INVALID_ARGUMENT |
| Entity not found (GET/DELETE on unknown id) | NOT_FOUND |
| Duplicate create | ALREADY_EXISTS |
| No auth credentials | UNAUTHENTICATED |
| Authenticated but lacks permission | PERMISSION_DENIED |
| Rate limited / quota exceeded | RESOURCE_EXHAUSTED |
| Client-set deadline passed | DEADLINE_EXCEEDED |
| Server busy / restarting (retry safe) | UNAVAILABLE |
| Unexpected DB error, panics, etc. | INTERNAL |
| RPC not implemented yet | UNIMPLEMENTED |
| Client explicitly cancelled | CANCELLED |
| Optimistic locking failure | ABORTED |
| State machine precondition not met | FAILED_PRECONDITION |
Server — Returning Errors
Unary
function getUser(call, callback) { if (!call.request.user_id) { return callback({ code: grpc.status.INVALID_ARGUMENT, message: 'user_id is required', }); } try { const user = db.findUser(call.request.user_id); if (!user) { return callback({ code: grpc.status.NOT_FOUND, message: `user ${call.request.user_id} not found`, details: 'searched in users table', }); } callback(null, { user }); } catch (err) { callback({ code: grpc.status.INTERNAL, message: 'internal server error', // never expose raw err.message to clients in production }); } }
Error with trailing metadata
function getUser(call, callback) { const trailer = new grpc.Metadata(); trailer.set('x-error-code', 'USR_NOT_FOUND'); callback({ code: grpc.status.NOT_FOUND, message: 'user not found', metadata: trailer, // trailing metadata on error path }); }
Server streaming error
function listUsers(call) { try { for (const user of db.getAll()) { call.write({ user }); } call.end(); } catch (err) { // emit error to terminate the stream with a status call.emit('error', { code: grpc.status.INTERNAL, message: err.message, }); // do NOT call call.end() after emitting error } }
Client — Handling Errors
Unary callback
client.getUser({ user_id: 42 }, (err, response) => { if (err) { switch (err.code) { case grpc.status.NOT_FOUND: console.error('User does not exist'); break; case grpc.status.UNAUTHENTICATED: console.error('Invalid token — refresh and retry'); break; case grpc.status.DEADLINE_EXCEEDED: console.error('Request timed out'); break; case grpc.status.UNAVAILABLE: console.error('Server unreachable — retry later'); break; default: console.error(`gRPC error ${err.code}: ${err.message}`); } return; } console.log(response.user); });
Async/await with error handling
const { promisify } = require('util'); const getUser = promisify(client.getUser.bind(client)); async function fetchUser(id) { try { return await getUser({ user_id: id }); } catch (err) { if (err.code === grpc.status.NOT_FOUND) return null; throw err; // re-throw unexpected errors } }
Stream error handling
const stream = client.listUsers({ active: true }); stream.on('data', (res) => handleUser(res.user)); stream.on('error', (err) => { // err.code === grpc.status.CANCELLED means WE called cancel() if (err.code === grpc.status.CANCELLED) return; console.error('stream failed:', err.code, err.message); }); stream.on('end', () => { // stream ended normally — no error console.log('done'); });
Rich Error Details (google.rpc.Status)
@grpc/grpc-js has no built-in rich-error API. The wire convention (shared by all languages): serialize a google.rpc.Status message — code, message, and repeated google.protobuf.Any details — into the trailing-metadata key grpc-status-details-bin. Load the well-known protos with protobufjs + google-proto-files:
npm install protobufjs google-proto-files
const protobuf = require('protobufjs'); const protos = require('google-proto-files'); const root = protobuf.loadSync([ protos.getProtoPath('rpc/status.proto'), protos.getProtoPath('rpc/error_details.proto'), ]); const Status = root.lookupType('google.rpc.Status'); const BadRequest = root.lookupType('google.rpc.BadRequest'); // Server: encode rich error into the trailer function createUser(call, callback) { const badRequest = BadRequest.create({ fieldViolations: [ { field: 'email', description: 'must be a valid email' }, { field: 'name', description: 'cannot be empty' }, ], }); const status = Status.create({ code: grpc.status.INVALID_ARGUMENT, message: 'validation failed', details: [{ typeUrl: 'type.googleapis.com/google.rpc.BadRequest', value: BadRequest.encode(badRequest).finish(), }], }); const trailer = new grpc.Metadata(); trailer.set('grpc-status-details-bin', Buffer.from(Status.encode(status).finish())); callback({ code: grpc.status.INVALID_ARGUMENT, message: 'validation failed', metadata: trailer, }); } // Client: decode the trailer client.createUser(request, (err, res) => { if (!err) return; const bin = err.metadata.get('grpc-status-details-bin')[0]; // Buffer (-bin keys are base64-decoded for you) if (!bin) return console.error(err.message); const status = Status.decode(bin); for (const any of status.details) { if (any.typeUrl.endsWith('google.rpc.BadRequest')) { BadRequest.decode(any.value).fieldViolations.forEach((v) => { console.error(` ${v.field}: ${v.description}`); }); } } });
Other languages have first-class helpers for the same trailer: Go status.WithDetails() / status.Details(), Python grpcio-status (rpc_status.to_status / from_call), Java io.grpc.protobuf.StatusProto.
Standard Rich Error Detail Types
All defined in google/rpc/error_details.proto (package google.rpc):
| Message | When to use |
|---|---|
BadRequest | Field-level validation errors |
PreconditionFailure | Pre-condition violations (e.g., account not verified) |
RetryInfo | Tell client when to retry after UNAVAILABLE / RESOURCE_EXHAUSTED |
QuotaFailure | Which quota was exceeded |
ResourceInfo | Which resource was NOT_FOUND or ALREADY_EXISTS |
RequestInfo | Request id for debugging |
ErrorInfo | Machine-readable reason + domain + metadata |
DebugInfo | Stack traces / internal debug data (never in production) |
Help | Documentation links |
LocalizedMessage | Localized error message for display |
Retry Logic
// Manual exponential back-off for UNAVAILABLE async function callWithRetry(fn, maxAttempts = 4) { let delay = 100; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (err) { const retryable = [grpc.status.UNAVAILABLE, grpc.status.RESOURCE_EXHAUSTED]; if (!retryable.includes(err.code) || attempt === maxAttempts) throw err; await new Promise((r) => setTimeout(r, delay)); delay = Math.min(delay * 2, 5_000); // cap at 5s } } } const user = await callWithRetry(() => getUser({ user_id: 1 }));
Error Logging Best Practices
// Server: log error with context, return safe message to client function getUser(call, callback) { try { const user = db.findUser(call.request.user_id); callback(null, { user }); } catch (err) { // Log full error internally with request context logger.error({ method: 'GetUser', peer: call.getPeer(), request: call.request, err, // full stack trace }); // Return minimal safe error to client callback({ code: grpc.status.INTERNAL, message: 'internal error', // no internal details exposed }); } }
Gotchas
DEADLINE_EXCEEDEDvsCANCELLED—DEADLINE_EXCEEDEDfires when the client's own deadline passes.CANCELLEDfires when.cancel()is called explicitly. They are mutually exclusive.
Do not call
callbackafter emitting error on streams — once you emit an error on a streaming call, the call is terminated. Callingend()orwrite()afterward may throw.
UNAVAILABLEis retry-safe;INTERNALis not — never auto-retryINTERNALerrors; they indicate unexpected server state.
Never expose raw
err.messagefrom DB/OS to clients — log it server-side, return a genericINTERNALmessage. Leaking stack traces / SQL errors is a security issue.
grpc.status.OKis 0 — a falsy checkif (err)works, butif (err.code)will be falsy forOK. Always checkif (err)notif (err.code).