RPC Status Codes & Errors

gRPC status codes and error handling in JustScale

The @justscale/rpc package provides a complete implementation of gRPC status codes and error handling. Throw typed errors in your handlers and they'll be properly transmitted to clients.

GrpcStatus Enum

All 17 standard gRPC status codes are available:

CodeNameDescription
0OKNot an error; returned on success
1CANCELLEDOperation was cancelled by the caller
2UNKNOWNUnknown error
3INVALID_ARGUMENTClient specified an invalid argument
4DEADLINE_EXCEEDEDDeadline expired before operation completed
5NOT_FOUNDRequested entity was not found
6ALREADY_EXISTSEntity already exists
7PERMISSION_DENIEDCaller lacks permission
8RESOURCE_EXHAUSTEDResource has been exhausted (quota, etc.)
9FAILED_PRECONDITIONSystem not in required state
10ABORTEDOperation aborted (concurrency issue)
11OUT_OF_RANGEOperation attempted past valid range
12UNIMPLEMENTEDOperation not implemented or supported
13INTERNALInternal server error
14UNAVAILABLEService currently unavailable
15DATA_LOSSUnrecoverable data loss or corruption
16UNAUTHENTICATEDInvalid authentication credentials
TypeScript
import { GrpcStatus } from '@justscale/rpc'

// Access status codes
GrpcStatus.OK              // 0
GrpcStatus.NOT_FOUND       // 5
GrpcStatus.INTERNAL        // 13
GrpcStatus.UNAUTHENTICATED // 16

GrpcError Class

The GrpcError class represents gRPC errors with status codes. Throw these in your handlers and they'll be properly serialized and sent to clients.

TypeScript
import { GrpcError, GrpcStatus } from '@justscale/rpc'

// Basic error
throw new GrpcError(GrpcStatus.NOT_FOUND, 'User not found')

// With additional details
throw new GrpcError(GrpcStatus.INVALID_ARGUMENT, 'Invalid email format', {
  field: 'email',
  value: 'not-an-email',
})

GrpcError provides useful properties:

  • code - The numeric gRPC status code
  • message - Human-readable error message
  • details - Optional additional error context
  • statusName - Human-readable status name (e.g., "NOT_FOUND")
TypeScript
const error = new GrpcError(GrpcStatus.NOT_FOUND, 'User not found')

error.code       // 5
error.message    // "User not found"
error.statusName // "NOT_FOUND"
error.toString() // "GrpcError [NOT_FOUND]: User not found"

Error Helper Functions

The grpcError helper provides factory functions for each status code, making error creation more readable:

TypeScript
import { grpcError } from '@justscale/rpc'

// Not found
throw grpcError.notFound('User not found')

// Invalid argument with details
throw grpcError.invalidArgument('Email is invalid', { field: 'email' })

// Permission denied
throw grpcError.permissionDenied('Admin access required')

// Unauthenticated
throw grpcError.unauthenticated('Token expired')

// All available helpers:
grpcError.cancelled(message, details?)
grpcError.invalidArgument(message, details?)
grpcError.deadlineExceeded(message, details?)
grpcError.notFound(message, details?)
grpcError.alreadyExists(message, details?)
grpcError.permissionDenied(message, details?)
grpcError.resourceExhausted(message, details?)
grpcError.failedPrecondition(message, details?)
grpcError.aborted(message, details?)
grpcError.outOfRange(message, details?)
grpcError.unimplemented(message, details?)
grpcError.internal(message, details?)
grpcError.unavailable(message, details?)
grpcError.dataLoss(message, details?)
grpcError.unauthenticated(message, details?)

Error Handling in Handlers

Throw GrpcError in your RPC handlers. The framework catches them and sends the appropriate status code to clients:

TypeScript
import { grpcError, isGrpcError } from '@justscale/rpc'

const UserController = createController
  .implements(UserServiceContract)
  .create({
    inject: { users: UserRepository },
    methods: ({ users }) => ({
      GetUser: async ({ body }) => {
        const user = await users.get(User.ref`${body.id}`)
        if (!user) {
          throw grpcError.notFound(`User ${body.id} not found`)
        }
        return user
      },

      CreateUser: async ({ body }) => {
        const existing = await users.findByEmail(body.email)
        if (existing) {
          throw grpcError.alreadyExists('Email already registered')
        }
        return users.create(body)
      },

      UpdateUser: async ({ body }) => {
        if (!body.id) {
          throw grpcError.invalidArgument('User ID is required')
        }
        // ...
      },
    }),
  })

Use isGrpcError to check if an error is a GrpcError:

TypeScript
import { isGrpcError, GrpcStatus } from '@justscale/rpc'

try {
  await client.GetUser({ id: 'unknown' })
} catch (err) {
  if (isGrpcError(err) && err.code === GrpcStatus.NOT_FOUND) {
    // Handle not found specifically
  }
}

Automatic Error Mapping

The errorToGrpcStatus function automatically maps common errors to appropriate gRPC status codes based on error message patterns:

TypeScript
import { errorToGrpcStatus, GrpcError } from '@justscale/rpc'

// Automatically maps error messages to status codes
const error = new Error('User not found')
const { code, message } = errorToGrpcStatus(error)
// code = GrpcStatus.NOT_FOUND (5)

// Pattern matching:
// "not found", "does not exist"  -> NOT_FOUND
// "unauthorized", "invalid token" -> UNAUTHENTICATED
// "permission denied", "forbidden" -> PERMISSION_DENIED
// "already exists", "duplicate"   -> ALREADY_EXISTS
// "invalid", "validation"         -> INVALID_ARGUMENT
// "timeout", "deadline"           -> DEADLINE_EXCEEDED
// "cancelled", "aborted"          -> CANCELLED
// "unavailable", "connection refused" -> UNAVAILABLE
// "exhausted", "rate limit", "quota" -> RESOURCE_EXHAUSTED
// Everything else                  -> INTERNAL

You can also convert any error to a GrpcError using GrpcError.from():

TypeScript
import { GrpcError } from '@justscale/rpc'

// Convert any error to GrpcError
const anyError = new Error('Something went wrong')
const grpcErr = GrpcError.from(anyError)
// Returns GrpcError with INTERNAL status

// GrpcErrors pass through unchanged
const existing = grpcError.notFound('User not found')
GrpcError.from(existing) === existing // true