Error Handling
Error handling in oRPC is flexible and consistent. You can use the ORPCError class, define typesafe errors, and adapt custom error classes while still returning meaningful feedback to clients.
ORPCError Class
ORPCError is the standard error type in oRPC. It includes a code, plus optional message and data fields.
DANGER
message and data are sent to the client. Do not include sensitive information in either field.
import { ORPCError, os } from '@orpc/server'
const rateLimitMiddleware = os.middleware(async ({ next }) => {
throw new ORPCError('RATE_LIMITED', {
message: 'You are being rate limited',
data: { retryAfter: 60 }
})
return next()
})
const example = os
.use(rateLimitMiddleware)
.handler(async ({ input }) => {
if (notFound) {
throw new ORPCError('NOT_FOUND')
}
})Typesafe Errors
For end-to-end type safety, define your errors with .errors or return ORPCError. This lets the client infer each error's shape and handle it safely. You can use any Standard Schema library to validate error data.
DANGER
message and data are sent to the client. Do not include sensitive information in either field.
const rateLimitMiddleware = os
.errors({
RATE_LIMITED: {
data: z.object({
retryAfter: z.number(),
}),
},
})
.middleware(async ({ next, errors }) => {
throw errors.RATE_LIMITED({
message: 'You are being rate limited',
data: { retryAfter: 60 }
})
return next()
})
const exampleProcedure = os
.use(rateLimitMiddleware)
.errors({
NOT_FOUND: {
message: 'The resource was not found', // <- default message
},
})
.handler(async ({ input, errors }) => {
if (notFound) {
throw errors.NOT_FOUND()
}
})TIP
You can use typesafe errors across your entire project, but we recommend reserving them for application-specific cases. For common errors like UNAUTHORIZED or RATE_LIMITED, the client usually already understands the meaning. Skipping explicit schemas for those errors can also reduce type complexity and improve TypeScript performance.
ORPCError Compatibility
If you cannot access the errors object, for example in a utility function or another module, you can still throw ORPCError. oRPC will try to convert it to the matching typesafe error when its code and data match a defined error. If no match is found, it is treated as an unknown error.
const exampleProcedure = os
.errors({
NOT_FOUND: {
message: 'The resource was not found',
},
})
.handler(async ({ errors }) => {
throw errors.NOT_FOUND()
// Treated as errors.NOT_FOUND because the code and data match
throw new ORPCError('NOT_FOUND')
// Treated as an unknown error because it does not match any defined error
throw new ORPCError('BAD_REQUEST')
})Returning an ORPCError
As an alternative to .errors, you can return an ORPCError directly from your handler or middleware to achieve end-to-end type safety.
WARNING
When implementing a contract, returning an ORPCError is equivalent to throwing one.
const exampleProcedure = os
.handler(async ({ errors }) => {
if (reachRateLimit) {
return new ORPCError('RATE_LIMITED', {
message: 'You are being rate limited',
data: { retryAfter: 60 }
})
}
return 'Success'
})DANGER
message and data are sent to the client. Do not include sensitive information in either field.
ORPC Error Codes
By default, oRPC allows any string as an error code and suggests common HTTP codes like NOT_FOUND and UNAUTHORIZED. You can override this with your own set of allowed error codes for better type safety and consistency.
declare module '@orpc/server' { // or '@orpc/client'
interface Registry {
ORPCErrorCode: 'NOT_FOUND' | 'UNAUTHORIZED' | 'RATE_LIMITED' | 'MY_CUSTOM_ERROR' | (string & {})
}
}With this configuration, only NOT_FOUND, UNAUTHORIZED, RATE_LIMITED, and MY_CUSTOM_ERROR will be suggested as error codes. The (string & {}) fallback ensures you can still use any string value when needed.
Using Custom Error Classes
You do not have to use ORPCError directly in your business logic. You can throw your own error classes and convert them to ORPCError in middleware or interceptors.
INFO
By default, oRPC can convert non-ORPCError into an ORPCError with code INTERNAL_SERVER_ERROR, or leave them unchanged depending on the client you are using.
class MyCustomError extends Error {
}
const customErrorConverterMiddleware = os.middleware(async ({ next }) => {
try {
return await next()
}
catch (err) {
if (err instanceof MyCustomError) {
throw new ORPCError('MY_CUSTOM_ERROR', { message: err.message, cause: err })
}
throw err
}
})Client Error Handling
To learn how to handle errors on the client side, see the Client Error Handling documentation.

