error-handling
coreThrow typed errors with TRPCError and error codes (NOT_FOUND, UNAUTHORIZED, BAD_REQUEST, INTERNAL_SERVER_ERROR), configure errorFormatter for client-side Zod error display, handle errors globally with onError callback, map tRPC errors to HTTP status codes with getHTTPStatusCodeFromError().
tRPC -- Error Handling
Setup
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { ZodError } from 'zod';
const t = initTRPC.create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
export const router = t.router;
export const publicProcedure = t.procedure;
Core Patterns
Throwing typed errors from procedures
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
userById: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
const user = getUserFromDb(input.id);
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `User with id ${input.id} not found`,
});
}
return user;
}),
});
function getUserFromDb(id: string) {
if (id === '1') return { id: '1', name: 'Katt' };
return null;
}
Wrapping original errors with cause
import { TRPCError } from '@trpc/server';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
riskyOperation: publicProcedure.mutation(async () => {
try {
return await externalService();
} catch (err) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred, please try again later.',
cause: err,
});
}
}),
});
async function externalService() {
throw new Error('connection refused');
}
Pass the original error as cause to retain the stack trace for debugging.
Global error handling with onError
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
const server = createHTTPServer({
router: appRouter,
onError(opts) {
const { error, type, path, input, ctx, req } = opts;
console.error('Error:', error);
if (error.code === 'INTERNAL_SERVER_ERROR') {
// send to bug reporting service
}
},
});
server.listen(3000);
Extracting HTTP status from TRPCError
import { TRPCError } from '@trpc/server';
import { getHTTPStatusCodeFromError } from '@trpc/server/http';
function handleError(error: unknown) {
if (error instanceof TRPCError) {
const httpCode = getHTTPStatusCodeFromError(error);
console.log(httpCode); // e.g., 400, 401, 404, 500
}
}
Common Mistakes
[HIGH] Throwing plain Error instead of TRPCError
Wrong:
import { publicProcedure } from './trpc';
const proc = publicProcedure.query(() => {
throw new Error('Not found');
// client receives 500 INTERNAL_SERVER_ERROR
});
Correct:
import { TRPCError } from '@trpc/server';
import { publicProcedure } from './trpc';
const proc = publicProcedure.query(() => {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'User not found',
});
// client receives 404 NOT_FOUND
});
Plain Error objects are caught and wrapped as INTERNAL_SERVER_ERROR (500); use TRPCError with a specific code for proper HTTP status mapping.
Source: www/docs/server/error-handling.md
[MEDIUM] Expecting stack traces in production
Wrong:
import { initTRPC } from '@trpc/server';
// No explicit isDev setting
const t = initTRPC.create();
// Stack traces may or may not appear depending on NODE_ENV
Correct:
import { initTRPC } from '@trpc/server';
const t = initTRPC.create({
isDev: process.env.NODE_ENV === 'development',
});
Stack traces are included only when isDev is true (default: NODE_ENV !== "production"); set isDev explicitly for deterministic behavior across runtimes.
Source: www/docs/server/error-handling.md
[HIGH] Not handling Zod errors in errorFormatter
Wrong:
import { initTRPC } from '@trpc/server';
// No errorFormatter -- client gets generic "Input validation failed"
const t = initTRPC.create();
Correct:
import { initTRPC } from '@trpc/server';
import { ZodError } from 'zod';
const t = initTRPC.create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
Without a custom errorFormatter, the client receives a generic message without field-level validation details from Zod.
Source: www/docs/server/error-formatting.md
Error Code Reference
| Code | HTTP | Use when |
|---|---|---|
| BAD_REQUEST | 400 | Invalid input |
| UNAUTHORIZED | 401 | Missing or invalid auth credentials |
| FORBIDDEN | 403 | Authenticated but not authorized |
| NOT_FOUND | 404 | Resource does not exist |
| CONFLICT | 409 | Request conflicts with current state |
| UNPROCESSABLE_CONTENT | 422 | Valid syntax but semantic error |
| TOO_MANY_REQUESTS | 429 | Rate limit exceeded |
| INTERNAL_SERVER_ERROR | 500 | Unexpected server error |
See Also
- server-setup -- initTRPC configuration including isDev
- validators -- input validation that triggers BAD_REQUEST errors
- middlewares -- auth middleware throwing UNAUTHORIZED
- server-side-calls -- catching TRPCError in server-side callers