Skip to content

tRPC v12 isn’t on npm yet. The @trpc12/* packages on this site can’t be installed until it’s released. Use v11 today

tRPC

Errors without try/catch

safe() returns a result tuple whose error lists every way a call can fail; switch on its code, or let matchError() check you handled each one.

On this page

safe() wraps a call and resolves to [data, error] instead of rejecting. Check error first: once it's handled, data has the output's type, with no undefined left to check:

import { 
function safe<T, E = unknown>(promise: PromiseLike<T> & {
    readonly "~error"?: E;
}): Promise<SafeResult<T, E>>
safe
} from '@trpc12/trpc/client';
async function function title(id: string): Promise<string>title(id: stringid: string) { const [
const post: {
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
} | undefined
post
,
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | undefined
err
] = await
safe<{
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}, (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | (TrpcError<...> & {
    ...;
})>(promise: PromiseLike<...> & {
    ...;
}): Promise<...>
safe
(
const client: TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<...>>
client
.
post: ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }>;
    list: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: undefined;
        output: Post[];
        errors: never;
        services: never;
    }>;
    create: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: TrpcError<...> | SchemaValidationError<...>;
        services: never;
    }>;
}, {}, undefined, LinksTypes<...>>
post
.
byId: {
    query(input: {
        id: string;
    }, opts?: CallOptions<{}> | undefined): TrpcPromise<{
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    }, CallError<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }, JsonForm, never>>;
}
byId
.
function query(input: {
    id: string;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<{
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}, CallError<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: Post;
    errors: TrpcError<"NOT_FOUND", {
        id: string;
    }> | SchemaValidationError<ZodObject<{
        id: ZodString;
    }, $strip>>;
    services: never;
}, JsonForm, never>>
query
({ id: stringid }));
if (
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | undefined
err
) return
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 19 more ... | (TrpcError<...> & {
    ...;
})
err
.Error.message: stringmessage;
return post.title: stringtitle;
const post: {
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}
}

Don't check data instead: an output can be undefined, null or 0 too, and the error would go unhandled. The result is also an object, { data, error }, for code that reads it without destructuring.

createSafeClient(client) returns a client whose every query and mutation is safe:

import { function createSafeClient<TClient extends object>(client: TClient): SafeClient<TClient>createSafeClient } from '@trpc12/trpc/client';

const 
const safeClient: SafeClient<TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<...>>>
safeClient
=
createSafeClient<TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<...>>>(client: TrpcClient<...>): SafeClient<...>
createSafeClient
(
const client: TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<...>>
client
);
const [
const post: {
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
} | undefined
post
,
const err: ClientError<TrpcError<"NOT_FOUND", {
    id: string;
}> | SchemaValidationError<ZodObject<{
    id: ZodString;
}, $strip>>, JsonForm> | undefined
err
] = await
const safeClient: SafeClient<TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<...>>>
safeClient
.
post: SafeClient<ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }>;
    list: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: undefined;
        output: Post[];
        errors: never;
        services: never;
    }>;
    create: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: TrpcError<...> | SchemaValidationError<...>;
        services: never;
    }>;
}, {}, undefined, LinksTypes<...>>>
post
.
byId: SafeClient<{
    query(input: {
        id: string;
    }, opts?: CallOptions<{}> | undefined): TrpcPromise<{
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    }, CallError<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }, JsonForm, never>>;
}>
byId
.
function query(input: {
    id: string;
}, opts?: CallOptions<{}> | undefined): Promise<SafeResult<{
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}, ClientError<TrpcError<"NOT_FOUND", {
    id: string;
}> | SchemaValidationError<ZodObject<{
    id: ZodString;
}, $strip>>, JsonForm>>>
query
({ id: stringid: '1' });

What a call can fail with

error is a TrpcError, and its type lists every way the call can fail. Each member has a literal code, in one of two groups:

  • Defined errors (defined: true): the procedure's own. They're the errors it returns, fails with or declares on the server (Errors), its input's BAD_REQUEST with data.issues, and errors links declare (Custom links). Each code has its data type.
  • Built-in errors (defined: false, the BuiltinError type): one member per built-in code, and they never have data.
Built-in codeWhen
NETWORK_ERRORThe request got no response.
CLIENT_CLOSED_REQUESTThe call's signal aborted it.
BAD_RESPONSEThe response wasn't a tRPC one (a proxy's HTML page) or didn't decode.
STREAM_INTERRUPTEDA stream stopped without an end or an error.
CLIENT_ERRORA link failed, or failed with a custom code it didn't declare.
INTERNAL_SERVER_ERRORThe server crashed, or threw a custom code it didn't declare. The message is masked in production.
Any other code (CONFLICT)The server threw it without declaring it, a proxy answered with its status (401 is UNAUTHORIZED), or the server lacks the procedure (NOT_FOUND).

Switching on the code

switch (err.code) narrows the error to the members with that code. A code can be in both groups: the server's NOT_FOUND for a missing post, and a built-in NOT_FOUND when the server doesn't have the procedure (a client deployed before its server). Until err.defined says which, data is the defined error's type or undefined:

async function function title(id: string): Promise<string>title(id: stringid: string) {
  const [
const post: {
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
} | undefined
post
,
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | undefined
err
] = await
safe<{
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}, (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | (TrpcError<...> & {
    ...;
})>(promise: PromiseLike<...> & {
    ...;
}): Promise<...>
safe
(
const client: TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<...>>
client
.
post: ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }>;
    list: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: undefined;
        output: Post[];
        errors: never;
        services: never;
    }>;
    create: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: TrpcError<...> | SchemaValidationError<...>;
        services: never;
    }>;
}, {}, undefined, LinksTypes<...>>
post
.
byId: {
    query(input: {
        id: string;
    }, opts?: CallOptions<{}> | undefined): TrpcPromise<{
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    }, CallError<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }, JsonForm, never>>;
}
byId
.
function query(input: {
    id: string;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<{
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}, CallError<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: Post;
    errors: TrpcError<"NOT_FOUND", {
        id: string;
    }> | SchemaValidationError<ZodObject<{
        id: ZodString;
    }, $strip>>;
    services: never;
}, JsonForm, never>>
query
({ id: stringid }));
if (
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | undefined
err
) {
switch (
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 19 more ... | (TrpcError<...> & {
    ...;
})
err
.TrpcError<TCode extends string = string, TData = unknown>.code: "NOT_FOUND" | "BAD_REQUEST" | "UNAUTHORIZED" | "PARSE_ERROR" | "UNSUPPORTED_PROTOCOL" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "NETWORK_ERROR" | "BAD_RESPONSE" | "CLIENT_ERROR" | "STREAM_INTERRUPTED"code) {
case 'NOT_FOUND':
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"NOT_FOUND", {
    id: string;
}> & {
    readonly defined: true;
})
err
.data;
TrpcError<TCode extends string = string, TData = unknown>.data: {
    id: string;
} | undefined
return
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"NOT_FOUND", {
    id: string;
}> & {
    readonly defined: true;
})
err
.defined: booleandefined ? `No post ${
const err: TrpcError<"NOT_FOUND", {
    id: string;
}> & {
    readonly defined: true;
}
err
.
TrpcError<"NOT_FOUND", { id: string; }>.data: {
    id: string;
}
data
.id: stringid}` : 'Please reload';
case 'NETWORK_ERROR': return 'You are offline'; default: return
const err: (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | (TrpcError<"METHOD_NOT_SUPPORTED", undefined> & {
    readonly defined: false;
}) | ... 16 more ... | (TrpcError<...> & {
    ...;
})
err
.Error.message: stringmessage;
} } return
const post: {
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}
post
.title: stringtitle;
}

Handling every defined code

matchError(err, handlers) takes a handler per defined code, each getting that error with its data, and builtin for the built-in errors, which a switch on e.code narrows further. It returns what the handler returns:

import { function matchError<E extends AnyTrpcError, H extends ErrorHandlers<E> & { readonly [K in keyof H]: K extends keyof ErrorHandlers<E> ? unknown : never; }>(err: E, handlers: H): HandlerResult<H>matchError } from '@trpc12/trpc/client';

async function function title(id: string): Promise<string>title(id: stringid: string) {
  const [
const post: {
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
} | undefined
post
,
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | undefined
err
] = await
safe<{
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}, (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | (TrpcError<...> & {
    ...;
})>(promise: PromiseLike<...> & {
    ...;
}): Promise<...>
safe
(
const client: TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<...>>
client
.
post: ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }>;
    list: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: undefined;
        output: Post[];
        errors: never;
        services: never;
    }>;
    create: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: TrpcError<...> | SchemaValidationError<...>;
        services: never;
    }>;
}, {}, undefined, LinksTypes<...>>
post
.
byId: {
    query(input: {
        id: string;
    }, opts?: CallOptions<{}> | undefined): TrpcPromise<{
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    }, CallError<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }, JsonForm, never>>;
}
byId
.
function query(input: {
    id: string;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<{
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}, CallError<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: Post;
    errors: TrpcError<"NOT_FOUND", {
        id: string;
    }> | SchemaValidationError<ZodObject<{
        id: ZodString;
    }, $strip>>;
    services: never;
}, JsonForm, never>>
query
({ id: stringid }));
if (
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | undefined
err
) {
return
matchError<(TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 19 more ... | (TrpcError<...> & {
    ...;
}), {
    ...;
}>(err: (TrpcError<...> & {
    readonly defined: false;
}) | ... 25 more ... | (TrpcError<...> & {
    ...;
}), handlers: {
    ...;
}): string
matchError
(
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 19 more ... | (TrpcError<...> & {
    ...;
})
err
, {
type NOT_FOUND: (e: TrpcError<"NOT_FOUND", {
    id: string;
}> & {
    readonly defined: true;
}) => string
NOT_FOUND
: (
e: TrpcError<"NOT_FOUND", {
    id: string;
}> & {
    readonly defined: true;
}
e
) => `No post ${
e: TrpcError<"NOT_FOUND", {
    id: string;
}> & {
    readonly defined: true;
}
e
.
TrpcError<"NOT_FOUND", { id: string; }>.data: {
    id: string;
}
data
.id: stringid}`,
type BAD_REQUEST: (e: TrpcError<"BAD_REQUEST", SchemaInputErrorData<ZodObject<{
    id: ZodString;
}, $strip>>> & {
    readonly defined: true;
}) => string
BAD_REQUEST
: (
e: TrpcError<...> & {
    ...;
}
e
) =>
e: TrpcError<...> & {
    ...;
}
e
.
TrpcError<"BAD_REQUEST", SchemaInputErrorData<ZodObject<{ id: ZodString; }, $strip>>>.data: SchemaInputErrorData<ZodObject<{
    id: ZodString;
}, $strip>>
data
.
SchemaInputErrorData<ZodObject<{ id: ZodString; }, $strip>>.issues: readonly ({
    readonly code: "invalid_type";
    readonly expected: $ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["id"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly ["id"];
    readonly message: string;
} | {
    readonly code: "too_small";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    ... 4 more ...;
    readonly message: string;
} | ... 8 more ... | {
    ...;
})[]
issues
[0]?.message: stringmessage ?? 'Invalid id',
builtin: (e: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 17 more ... | (TrpcError<...> & {
    readonly defined: false;
})) => string
builtin
: (
e: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 17 more ... | (TrpcError<...> & {
    readonly defined: false;
})
e
) =>
e: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 17 more ... | (TrpcError<...> & {
    readonly defined: false;
})
e
.TrpcError<TCode extends string = string, TData = unknown>.code: "NOT_FOUND" | "BAD_REQUEST" | "UNAUTHORIZED" | "PARSE_ERROR" | "UNSUPPORTED_PROTOCOL" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "NETWORK_ERROR" | "BAD_RESPONSE" | "CLIENT_ERROR" | "STREAM_INTERRUPTED"code === 'NETWORK_ERROR' ? 'You are offline' :
e: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 16 more ... | (TrpcError<...> & {
    readonly defined: false;
})
e
.Error.message: stringmessage,
}); } return
const post: {
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}
post
.title: stringtitle;
}

A handler missing for one of the procedure's codes, or left over for a code it no longer has, is a type error, so adding an error on the server shows every place that must handle it:

const [, 
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | undefined
err
] = await
safe<{
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}, (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | (TrpcError<...> & {
    ...;
})>(promise: PromiseLike<...> & {
    ...;
}): Promise<...>
safe
(
const client: TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<...>>
client
.
post: ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }>;
    list: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: undefined;
        output: Post[];
        errors: never;
        services: never;
    }>;
    create: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: TrpcError<...> | SchemaValidationError<...>;
        services: never;
    }>;
}, {}, undefined, LinksTypes<...>>
post
.
byId: {
    query(input: {
        id: string;
    }, opts?: CallOptions<{}> | undefined): TrpcPromise<{
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    }, CallError<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }, JsonForm, never>>;
}
byId
.
function query(input: {
    id: string;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<{
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}, CallError<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: Post;
    errors: TrpcError<"NOT_FOUND", {
        id: string;
    }> | SchemaValidationError<ZodObject<{
        id: ZodString;
    }, $strip>>;
    services: never;
}, JsonForm, never>>
query
({ id: stringid: '1' }));
if (
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | undefined
err
) {
matchError<(TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 19 more ... | (TrpcError<...> & {
    ...;
}), {
    ...;
} & ... 1 more ... & {}>(err: (TrpcError<...> & {
    readonly defined: false;
}) | ... 25 more ... | (TrpcError<...> & {
    ...;
}), handlers: {
    ...;
} & ... 1 more ... & {}): unknown
matchError
(
const err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 19 more ... | (TrpcError<...> & {
    ...;
})
err
, {
Argument of type '{ NOT_FOUND: (e: TrpcError<"NOT_FOUND", { id: string; }> & { readonly defined: true; }) => string; builtin: (e: (TrpcError<"NOT_FOUND", undefined> & { readonly defined: false; }) | ... 23 more ... | (TrpcError<...> & { ...; })) => string; }' is not assignable to parameter of type '{ readonly NOT_FOUND: (err: TrpcError<"NOT_FOUND", { id: string; }> & { readonly defined: true; }) => unknown; readonly BAD_REQUEST: (err: TrpcError<"BAD_REQUEST", SchemaInputErrorData<ZodObject<{ id: ZodString; }, $strip>>> & { ...; }) => unknown; } & { ...; } & {}'. Property 'BAD_REQUEST' is missing in type '{ NOT_FOUND: (e: TrpcError<"NOT_FOUND", { id: string; }> & { readonly defined: true; }) => string; builtin: (e: (TrpcError<"NOT_FOUND", undefined> & { readonly defined: false; }) | ... 23 more ... | (TrpcError<...> & { ...; })) => string; }' but required in type '{ readonly NOT_FOUND: (err: TrpcError<"NOT_FOUND", { id: string; }> & { readonly defined: true; }) => unknown; readonly BAD_REQUEST: (err: TrpcError<"BAD_REQUEST", SchemaInputErrorData<ZodObject<{ id: ZodString; }, $strip>>> & { ...; }) => unknown; }'.
type NOT_FOUND: (err: TrpcError<"NOT_FOUND", {
    id: string;
}> & {
    readonly defined: true;
}) => unknown
NOT_FOUND
: (
e: TrpcError<"NOT_FOUND", {
    id: string;
}> & {
    readonly defined: true;
}
e
) => `No post ${
e: TrpcError<"NOT_FOUND", {
    id: string;
}> & {
    readonly defined: true;
}
e
.
TrpcError<"NOT_FOUND", { id: string; }>.data: {
    id: string;
}
data
.id: stringid}`,
builtin: (err: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 17 more ... | (TrpcError<...> & {
    readonly defined: false;
})) => unknown
builtin
: (
e: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 17 more ... | (TrpcError<...> & {
    readonly defined: false;
})
e
) =>
e: (TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"FORBIDDEN", undefined> & {
    readonly defined: false;
}) | ... 17 more ... | (TrpcError<...> & {
    readonly defined: false;
})
e
.Error.message: stringmessage,
}); }

A built-in error with a defined code, like the NOT_FOUND of a procedure the server lacks, goes to builtin.

Checking a caught value

isTrpcError(value) checks a value from a catch, and isDefinedError(error) narrows to the defined errors. In the Effect client the same union is the error channel, and an error's _tag is its code, so Effect.catchTag('NETWORK_ERROR', …) handles one member.

safe() doesn't catch a framework's control flow: Next's redirect() and notFound(), which in-process calls rethrow (Server Actions), still throw.

Credits

safe(), createSafeClient() and isDefinedError() follow oRPC's client error handling. The closed error union and matchError() follow result-rpc's errors as values.