Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

Client Error Handling

oRPC supports several ways to handle client-side errors. In most cases, try/catch is enough. If you use Typesafe Errors, safe and createSafeClient let you handle them with full type inference.

Using try/catch

For most calls, use regular try/catch.

try {
  const data = await client.doSomething({ id: '123' })
}
catch (error) {
  // handle error
}

Using safe and isInferableError

When working with Typesafe Errors, use safe to preserve error type inference. It behaves like try/catch, but returns the typesafe result instead of throwing.

import { function isInferableError<T>(error: T): error is Extract<T, AnyORPCError>
Checks if an error is an `ORPCError` whose type is inferable at the TypeScript level, narrowing it so `code` and `data` are fully typed.
@see{@link https://orpc.dev/docs/client/error-handling#using-safe-and-isinferableerror Client Error Handling - Using safe and isInferableError}
isInferableError
, function safe<TOutput, TError = Error>(promise: PromiseWithError<TOutput, TError>): Promise<SafeResult<TOutput, TError>>
Works like try/catch, but help you infer the error type if it is inferable ORPCError.
@example```ts const [error, data, inferableError, isSuccess] = await safe(client(...)) // or const { error, data, inferableError, isSuccess } = await safe(client(...)) if (inferableError) { console.log(inferableError) // or error, both are well typed } ```@see{@link https://orpc.dev/docs/client/error-handling#using-safe-and-isinferableerror Client Error Handling - Using safe and isInferableError}
safe
} from '@orpc/client'
const
const exampleProcedure: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>, Schema<never>, {
    RATE_LIMIT_EXCEEDED: {
        data: z.ZodObject<{
            retryAfter: z.ZodNumber;
        }, z.core.$strip>;
    };
}, 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>>.input<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>): BuilderWithInput<DefaultInitialContext & object, object, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>, Record<never, never>>
input
(import zz.
function object<{
    id: z.ZodString;
}>(shape?: {
    id: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>
object
({ id: z.ZodStringid: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.
BuilderWithInput<DefaultInitialContext & object, object, ZodObject<{ id: ZodString; }, $strip>, Record<never, never>>['errors']<{
    RATE_LIMIT_EXCEEDED: {
        data: z.ZodObject<{
            retryAfter: z.ZodNumber;
        }, z.core.$strip>;
    };
}>(errors: {
    RATE_LIMIT_EXCEEDED: {
        data: z.ZodObject<{
            retryAfter: z.ZodNumber;
        }, z.core.$strip>;
    };
}): BuilderWithInput<DefaultInitialContext & object, object, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>, {
    RATE_LIMIT_EXCEEDED: {
        data: z.ZodObject<{
            retryAfter: z.ZodNumber;
        }, z.core.$strip>;
    };
}>
errors
({
type RATE_LIMIT_EXCEEDED: {
    data: z.ZodObject<{
        retryAfter: z.ZodNumber;
    }, z.core.$strip>;
}
RATE_LIMIT_EXCEEDED
: {
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() })
} }) .
BuilderWithInput<DefaultInitialContext & object, object, ZodObject<{ id: ZodString; }, $strip>, { RATE_LIMIT_EXCEEDED: { ...; }; }>['handler']<never>(handler: ProcedureHandler<DefaultInitialContext & object, {
    id: string;
}, never, ORPCErrorConstructorMap<{
    RATE_LIMIT_EXCEEDED: {
        data: z.ZodObject<{
            retryAfter: z.ZodNumber;
        }, z.core.$strip>;
    };
}>>): DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>, Schema<never>, {
    RATE_LIMIT_EXCEEDED: {
        data: z.ZodObject<{
            retryAfter: z.ZodNumber;
        }, z.core.$strip>;
    };
}, never>
handler
(async ({
input: {
    id: string;
}
input
,
errors: ORPCErrorConstructorMap<{
    RATE_LIMIT_EXCEEDED: {
        data: z.ZodObject<{
            retryAfter: z.ZodNumber;
        }, z.core.$strip>;
    };
}>
errors
}) => {
throw
errors: ORPCErrorConstructorMap<{
    RATE_LIMIT_EXCEEDED: {
        data: z.ZodObject<{
            retryAfter: z.ZodNumber;
        }, z.core.$strip>;
    };
}>
errors
.
type RATE_LIMIT_EXCEEDED: ORPCErrorConstructorMapItem
(options: ORPCErrorConstructorMapItemOptions<{
    retryAfter: number;
}>) => ORPCError<"RATE_LIMIT_EXCEEDED", {
    retryAfter: number;
}>
RATE_LIMIT_EXCEEDED
({
data: {
    retryAfter: number;
}
data
: { retryAfter: numberretryAfter: 1000 } })
}) // or { error, data, inferableError } const [
const error: Error | ORPCError<"RATE_LIMIT_EXCEEDED", {
    retryAfter: number;
}> | null
error
, const data: undefineddata,
const inferableError: ORPCError<"RATE_LIMIT_EXCEEDED", {
    retryAfter: number;
}> | null
inferableError
] = await
safe<never, Error | ORPCError<"RATE_LIMIT_EXCEEDED", {
    retryAfter: number;
}>>(promise: PromiseWithError<never, Error | ORPCError<"RATE_LIMIT_EXCEEDED", {
    retryAfter: number;
}>>): Promise<SafeResult<never, Error | ORPCError<"RATE_LIMIT_EXCEEDED", {
    retryAfter: number;
}>>>
Works like try/catch, but help you infer the error type if it is inferable ORPCError.
@example```ts const [error, data, inferableError, isSuccess] = await safe(client(...)) // or const { error, data, inferableError, isSuccess } = await safe(client(...)) if (inferableError) { console.log(inferableError) // or error, both are well typed } ```@see{@link https://orpc.dev/docs/client/error-handling#using-safe-and-isinferableerror Client Error Handling - Using safe and isInferableError}
safe
(
call<DefaultInitialContext & object, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>, Schema<never>, {
    RATE_LIMIT_EXCEEDED: {
        data: z.ZodObject<{
            retryAfter: z.ZodNumber;
        }, z.core.$strip>;
    };
}, never>(lazyableProcedure: Lazyable<Procedure<DefaultInitialContext & object, any, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>, Schema<never>, {
    RATE_LIMIT_EXCEEDED: {
        data: z.ZodObject<{
            retryAfter: z.ZodNumber;
        }, z.core.$strip>;
    };
}, never>>, input: {
    ...;
}, options?: CallOptions<...> | undefined): PromiseWithError<...>
Quickly call a procedure without creating a client.
@example```ts const output = await call(getting, 'input') const output = await call(getting, 'input', { context: { db: 'postgres' } }) ```@see{@link https://orpc.dev/docs/client/server-side#one-off-calls Server-Side Clients - One-Off Calls}
call
(
const exampleProcedure: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>, Schema<never>, {
    RATE_LIMIT_EXCEEDED: {
        data: z.ZodObject<{
            retryAfter: z.ZodNumber;
        }, z.core.$strip>;
    };
}, never>
exampleProcedure
, { id: stringid: '123' })
) if (
isInferableError<Error | ORPCError<"RATE_LIMIT_EXCEEDED", {
    retryAfter: number;
}> | null>(error: Error | ORPCError<"RATE_LIMIT_EXCEEDED", {
    retryAfter: number;
}> | null): error is ORPCError<"RATE_LIMIT_EXCEEDED", {
    retryAfter: number;
}>
Checks if an error is an `ORPCError` whose type is inferable at the TypeScript level, narrowing it so `code` and `data` are fully typed.
@see{@link https://orpc.dev/docs/client/error-handling#using-safe-and-isinferableerror Client Error Handling - Using safe and isInferableError}
isInferableError
(
const error: Error | ORPCError<"RATE_LIMIT_EXCEEDED", {
    retryAfter: number;
}> | null
error
)) { // or inferableError
// handle inferable error // or inferableError.data.retryAfter var console: Consoleconsole.Console.log(...data: any[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(
const error: ORPCError<"RATE_LIMIT_EXCEEDED", {
    retryAfter: number;
}>
error
.
ORPCError<"RATE_LIMIT_EXCEEDED", { retryAfter: number; }>.data: {
    retryAfter: number;
}
data
.retryAfter: numberretryAfter)
} else if (const error: Error | nullerror) { // handle unknown error } else { // handle success var console: Consoleconsole.Console.log(...data: any[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(const data: neverdata)
}

Safe Client

If you use safe often, createSafeClient can reduce repetition by wrapping entire client calls with safe.

import { createSafeClient } from '@orpc/client'

const safeClient = createSafeClient(client)

const [error, data] = await safeClient.doSomething({ id: '123' })

Last updated on August 6, 2026

Was this page helpful?