Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

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.
@see{@link https://orpc.dev/docs/error-handling#orpcerror-class Error Handling - ORPCError Class}
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.
@see{@link https://orpc.dev/docs/procedure Procedure}
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.
@see{@link https://orpc.dev/docs/procedure Procedure}
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.
@see{@link https://orpc.dev/docs/error-handling#orpcerror-class Error Handling - ORPCError Class}
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.
@see{@link https://orpc.dev/docs/procedure Procedure}
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.
@see{@link https://orpc.dev/docs/error-handling#orpcerror-class Error Handling - ORPCError Class}
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.
@see{@link https://orpc.dev/docs/procedure Procedure}
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.
@see{@link https://orpc.dev/docs/procedure Procedure}
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.

Last updated on August 6, 2026

Was this page helpful?