Error Handling
A failed procedure call rejects with a TRPCClientError. You can either let it throw and catch it, or use safe() to get it back as a type-safe value.
Catching a thrown error
query() and mutate() behave like any other promise, so a try/catch works as you would expect. TypeScript has no way to describe what a function throws, though, so cause arrives as unknown and has to be narrowed:
client.tstsimport { TRPCClientError } from '@trpc/client';import type { AppRouter } from './server';import{ trpc } from './trpc'; try {const post = await trpc.post.byId.query('1');} catch (cause) {if (cause instanceof TRPCClientError) {console.log(cause.data);} else {// something that isn't a tRPC error }}
client.tstsimport { TRPCClientError } from '@trpc/client';import type { AppRouter } from './server';import{ trpc } from './trpc'; try {const post = await trpc.post.byId.query('1');} catch (cause) {if (cause instanceof TRPCClientError) {console.log(cause.data);} else {// something that isn't a tRPC error }}
See Inferring Types for a reusable isTRPCClientError guard.
Errors as values with safe()
safe() awaits a query() or mutate() call without letting it throw, handing back a [data, error] pair instead. Exactly one side is ever set, so checking either one narrows the other:
client.tstsimport { safe } from '@trpc/client';import { trpc } from './trpc';const [post, error] = await safe(trpc.post.byId.query('1'));if (error) {console.log(error.data);} else {console.log(post.title);}
client.tstsimport { safe } from '@trpc/client';import { trpc } from './trpc';const [post, error] = await safe(trpc.post.byId.query('1'));if (error) {console.log(error.data);} else {console.log(post.title);}
Subscriptions don't need safe() - they never throw at the call site, and report failures through onError, which is already typed with the procedure's error shape.