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.
import { class ORPCError<TCode extends ORPCErrorCode, TData>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError, const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os } from '@orpc/server'
const const rateLimitMiddleware: DecoratedMiddleware<DefaultInitialContext & object, object, unknown, any, Record<never, never>>rateLimitMiddleware = const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os.Builder<DefaultInitialContext & object, Record<never, never>>.middleware<object, unknown, any>(middleware: Middleware<DefaultInitialContext & object, object, unknown, any, Record<never, never>>): DecoratedMiddleware<DefaultInitialContext & object, object, unknown, any, Record<never, never>>middleware(async ({ next: MiddlewareNext<any>Invoke to continue the middleware chain.next }) => {
throw new new ORPCError<"RATE_LIMITED", {
retryAfter: number;
}>(code: "RATE_LIMITED", options: ORPCErrorOptions<{
retryAfter: number;
}>): ORPCError<"RATE_LIMITED", {
retryAfter: number;
}>
Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError('RATE_LIMITED', {
message?: string | undefinedmessage: 'You are being rate limited',
data: {
retryAfter: number;
}
data: { retryAfter: numberretryAfter: 60 }
})
return next: MiddlewareNext
<object>(options?: {
context?: object | undefined;
} | undefined) => MiddlewareResult<object, any>
Invoke to continue the middleware chain.next()
})
const const example: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<void>, Record<never, never>, never>example = const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os
.Builder<DefaultInitialContext & object, Record<never, never>>.use<object, DefaultInitialContext & object, Record<never, never>>(middleware: Middleware<DefaultInitialContext & object, object, unknown, unknown, Record<never, never>>): BuilderWithMiddlewares<DefaultInitialContext & object, object, Record<never, never>>use(const rateLimitMiddleware: DecoratedMiddleware<DefaultInitialContext & object, object, unknown, any, Record<never, never>>rateLimitMiddleware)
.BuilderWithMiddlewares<DefaultInitialContext & object, object, Record<never, never>>['handler']<void>(handler: ProcedureHandler<DefaultInitialContext & object, unknown, void, ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<void>, Record<never, never>, never>handler(async ({ input: unknowninput }) => {
if (const notFound: booleannotFound) {
throw new new ORPCError<"NOT_FOUND", unknown>(code: "NOT_FOUND", options?: ORPCErrorOptions<unknown> | undefined): ORPCError<"NOT_FOUND", unknown>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.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.
const const rateLimitMiddleware: DecoratedMiddleware<DefaultInitialContext & object, object, unknown, any, {
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}>
rateLimitMiddleware = const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os
.Builder<DefaultInitialContext & object, Record<never, never>>.errors<{
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}>(errors: {
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}): Builder<DefaultInitialContext & object, {
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}>
errors({
type RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
}
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>
data: import zz.function object<{
retryAfter: z.ZodNumber;
}>(shape?: {
retryAfter: z.ZodNumber;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>
object({
retryAfter: z.ZodNumberretryAfter: import zz.function number(params?: string | z.core.$ZodNumberParams): z.ZodNumbernumber(),
}),
},
})
.Builder<DefaultInitialContext & object, { RATE_LIMITED: { data: ZodObject<{ retryAfter: ZodNumber; }, $strip>; }; }>.middleware<object, unknown, any>(middleware: Middleware<DefaultInitialContext & object, object, unknown, any, {
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}>): DecoratedMiddleware<DefaultInitialContext & object, object, unknown, any, {
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}>
middleware(async ({ next: MiddlewareNext<any>Invoke to continue the middleware chain.next, errors: ORPCErrorConstructorMap<{
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}>
errors }) => {
throw errors: ORPCErrorConstructorMap<{
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}>
errors.type RATE_LIMITED: ORPCErrorConstructorMapItem
(options: ORPCErrorConstructorMapItemOptions<{
retryAfter: number;
}>) => ORPCError<"RATE_LIMITED", {
retryAfter: number;
}>
RATE_LIMITED({
message?: string | undefinedmessage: 'You are being rate limited',
data: {
retryAfter: number;
}
data: { retryAfter: numberretryAfter: 60 }
})
return next: MiddlewareNext
<object>(options?: {
context?: object | undefined;
} | undefined) => MiddlewareResult<object, any>
Invoke to continue the middleware chain.next()
})
const const exampleProcedure: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<void>, Omit<Omit<{
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}, never> & Record<never, never>, "NOT_FOUND"> & {
NOT_FOUND: {
message: string;
};
}, never>
exampleProcedure = const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os
.Builder<DefaultInitialContext & object, Record<never, never>>.use<object, DefaultInitialContext & object, {
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}>(middleware: Middleware<DefaultInitialContext & object, object, unknown, unknown, {
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}>): BuilderWithMiddlewares<DefaultInitialContext & object, object, Omit<{
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}, never> & Record<...>>
use(const rateLimitMiddleware: DecoratedMiddleware<DefaultInitialContext & object, object, unknown, any, {
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}>
rateLimitMiddleware)
.BuilderWithMiddlewares<DefaultInitialContext & object, object, Omit<{ RATE_LIMITED: { data: ZodObject<{ retryAfter: ZodNumber; }, $strip>; }; }, never> & Record<...>>['errors']<{
NOT_FOUND: {
message: string;
};
}>(errors: {
NOT_FOUND: {
message: string;
};
}): BuilderWithMiddlewares<DefaultInitialContext & object, object, Omit<Omit<{
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}, never> & Record<never, never>, "NOT_FOUND"> & {
NOT_FOUND: {
message: string;
};
}>
errors({
type NOT_FOUND: {
message: string;
}
NOT_FOUND: {
message: stringmessage: 'The resource was not found', // <- default message
},
})
.BuilderWithMiddlewares<DefaultInitialContext & object, object, Omit<Omit<{ RATE_LIMITED: { data: ZodObject<{ retryAfter: ZodNumber; }, $strip>; }; }, never> & Record<...>, "NOT_FOUND"> & { ...; }>['handler']<void>(handler: ProcedureHandler<DefaultInitialContext & object, unknown, void, ORPCErrorConstructorMap<Omit<Omit<{
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}, never> & Record<never, never>, "NOT_FOUND"> & {
NOT_FOUND: {
message: string;
};
}>>): DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<void>, Omit<Omit<{
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}, never> & Record<...>, "NOT_FOUND"> & {
NOT_FOUND: {
message: string;
};
}, never>
handler(async ({ input: unknowninput, errors: ORPCErrorConstructorMap<Omit<Omit<{
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}, never> & Record<never, never>, "NOT_FOUND"> & {
NOT_FOUND: {
message: string;
};
}>
errors }) => {
if (const notFound: booleannotFound) {
throw errors: ORPCErrorConstructorMap<Omit<Omit<{
RATE_LIMITED: {
data: z.ZodObject<{
retryAfter: z.ZodNumber;
}, z.core.$strip>;
};
}, never> & Record<never, never>, "NOT_FOUND"> & {
NOT_FOUND: {
message: string;
};
}>
errors.type NOT_FOUND: ORPCErrorConstructorMapItem
(options?: ORPCErrorConstructorMapItemOptions<unknown> | undefined) => ORPCError<"NOT_FOUND", unknown>
NOT_FOUND()
}
})
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.
const exampleProcedure = os
.handler(async ({ errors }) => {
if (reachRateLimit) {
return new ORPCError('RATE_LIMITED', {
message: 'You are being rate limited',
data: { retryAfter: 60 }
})
}
return 'Success'
})
Error Factory
An error factory lets you define an error once and reuse it anywhere, keeping error handling consistent across your project.
import { error } from '@orpc/server'
const RateLimitedError = error('RATE_LIMITED', {
/**
* Optional default message, can be overridden when constructing an error.
*/
message: 'You are being rate limited',
/**
* Optional schema used to type and validate the error data.
* Must be a synchronous schema.
*/
data: z.object({
retryAfter: z.number(),
}),
})
const procedure = os
.handler(async () => {
throw new RateLimitedError({ data: { retryAfter: 60 } })
})
instanceof Support
An error factory class supports instanceof checks with full type narrowing. It matches any ORPCError with the same code whose data passes the schema.
if (err instanceof RateLimitedError) {
console.log(err.data.retryAfter)
}
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.
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.