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

SituationCode
Bad field value, missing required fieldINVALID_ARGUMENT
Entity not found (GET/DELETE on unknown id)NOT_FOUND
Duplicate createALREADY_EXISTS
No auth credentialsUNAUTHENTICATED
Authenticated but lacks permissionPERMISSION_DENIED
Rate limited / quota exceededRESOURCE_EXHAUSTED
Client-set deadline passedDEADLINE_EXCEEDED
Server busy / restarting (retry safe)UNAVAILABLE
Unexpected DB error, panics, etc.INTERNAL
RPC not implemented yetUNIMPLEMENTED
Client explicitly cancelledCANCELLED
Optimistic locking failureABORTED
State machine precondition not metFAILED_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):

MessageWhen to use
BadRequestField-level validation errors
PreconditionFailurePre-condition violations (e.g., account not verified)
RetryInfoTell client when to retry after UNAVAILABLE / RESOURCE_EXHAUSTED
QuotaFailureWhich quota was exceeded
ResourceInfoWhich resource was NOT_FOUND or ALREADY_EXISTS
RequestInfoRequest id for debugging
ErrorInfoMachine-readable reason + domain + metadata
DebugInfoStack traces / internal debug data (never in production)
HelpDocumentation links
LocalizedMessageLocalized 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_EXCEEDED vs CANCELLEDDEADLINE_EXCEEDED fires when the client's own deadline passes. CANCELLED fires when .cancel() is called explicitly. They are mutually exclusive.

Do not call callback after emitting error on streams — once you emit an error on a streaming call, the call is terminated. Calling end() or write() afterward may throw.

UNAVAILABLE is retry-safe; INTERNAL is not — never auto-retry INTERNAL errors; they indicate unexpected server state.

Never expose raw err.message from DB/OS to clients — log it server-side, return a generic INTERNAL message. Leaking stack traces / SQL errors is a security issue.

grpc.status.OK is 0 — a falsy check if (err) works, but if (err.code) will be falsy for OK. Always check if (err) not if (err.code).