Skip to main content
oRPC has first-class support for typed errors. You can declare the errors a procedure may throw, receive them as typed objects on the client, and use the safe() helper to handle them without try/catch boilerplate.

ORPCError

ORPCError is the error class used throughout oRPC. It extends Error and carries a code, HTTP status, and optional typed data:
import { ORPCError } from '@orpc/server'

// Built-in codes map to standard HTTP status codes
throw new ORPCError('UNAUTHORIZED')         // 401
throw new ORPCError('NOT_FOUND')            // 404
throw new ORPCError('INTERNAL_SERVER_ERROR') // 500

// Custom message
throw new ORPCError('FORBIDDEN', { message: 'Subscription required' })
Built-in codes include: BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, UNPROCESSABLE_CONTENT, TOO_MANY_REQUESTS, INTERNAL_SERVER_ERROR, and more.

Typed error maps with .errors()

Declare the errors a procedure can throw using .errors(). This makes the errors visible to clients and enables type-safe throwing:
import { os, ORPCError } from '@orpc/server'
import * as z from 'zod'

export const createPlanet = os
  .errors({
    PLANET_LIMIT_REACHED: {
      status: 429,
      message: 'Planet limit reached',
      data: z.object({
        limit: z.number(),
        current: z.number(),
      }),
    },
  })
  .input(z.object({ name: z.string() }))
  .handler(async ({ input, errors }) => {
    const count = await getPlanetCount()
    const limit = 100

    if (count >= limit) {
      // errors.PLANET_LIMIT_REACHED is a typed constructor
      throw errors.PLANET_LIMIT_REACHED({
        data: { limit, current: count },
      })
    }

    return { id: 1, name: input.name }
  })
Errors declared in .errors() are called defined errors — they have defined: true on the ORPCError instance. Undefined errors (generic throws) have defined: false.

Sharing errors across a router

You can attach an error map to the os builder so every procedure inherits the same errors:
import { os } from '@orpc/server'
import * as z from 'zod'

const base = os.errors({
  UNAUTHORIZED: {
    status: 401,
    message: 'Unauthorized',
  },
  FORBIDDEN: {
    status: 403,
    message: 'Forbidden',
  },
})

export const listPlanet = base.handler(async ({ errors }) => {
  // errors.UNAUTHORIZED and errors.FORBIDDEN are available here
  return []
})

The safe() helper

safe() wraps a procedure call and returns a tuple/object instead of throwing. It supports both tuple destructuring and object destructuring:
import { safe } from '@orpc/server'
import { orpc } from './client'

// Tuple style
const [error, data, isDefined] = await safe(orpc.planet.create({ name: 'Earth' }))

// Object style
const { error, data, isDefined, isSuccess } = await safe(
  orpc.planet.create({ name: 'Earth' })
)

if (error) {
  if (isDefined) {
    // error is a typed ORPCError defined in the procedure's error map
    console.log(error.code)   // e.g. 'PLANET_LIMIT_REACHED'
    console.log(error.data)   // typed according to the error's data schema
  } else {
    // Unexpected error (network failure, unhandled exception, etc.)
    console.error(error)
  }
} else {
  console.log(data) // typed procedure output
}
FieldTypeDescription
errorORPCError | nullThe error, or null on success
dataTOutput | undefinedThe output, or undefined on failure
isDefinedbooleantrue if the error is a declared typed error
isSuccessbooleantrue if the call succeeded

Re-throwing errors

If you catch an error in middleware and want to re-throw it with a different type, construct a new ORPCError:
os.middleware(async ({ next }) => {
  try {
    return await next()
  } catch (error) {
    if (error instanceof ORPCError && error.code === 'NOT_FOUND') {
      throw new ORPCError('FORBIDDEN', { cause: error })
    }
    throw error
  }
})

Using isDefinedError

The isDefinedError type guard narrows an unknown error to a typed ORPCError:
import { isDefinedError } from '@orpc/server'

try {
  await orpc.planet.create({ name: 'Earth' })
} catch (error) {
  if (isDefinedError(error)) {
    // error is narrowed to the declared error union
    console.log(error.code)
    console.log(error.data)
  }
}
Prefer safe() over try/catch in client code — it gives you typed errors without exception handling syntax, and works well with async/await flows.

Build docs developers (and LLMs) love