Skip to main content
Version: 11.x

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.ts
ts
import { 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.ts
ts
import { 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.ts
ts
import { 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.ts
ts
import { 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);
}
info

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.