| name | error-handling |
| description | Throw 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().
|
| type | core |
| library | trpc |
| library_version | 11.16.0 |
| requires | ["server-setup"] |
| sources | ["trpc/trpc:www/docs/server/error-handling.md","trpc/trpc:www/docs/server/error-formatting.md","trpc/trpc:packages/server/src/unstable-core-do-not-import/error/TRPCError.ts"] |
tRPC -- Error Handling
Setup
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') {
}
},
});
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);
}
}
Common Mistakes
[HIGH] Throwing plain Error instead of TRPCError
Wrong:
import { publicProcedure } from './trpc';
const proc = publicProcedure.query(() => {
throw new Error('Not found');
});
Correct:
import { TRPCError } from '@trpc/server';
import { publicProcedure } from './trpc';
const proc = publicProcedure.query(() => {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'User 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';
const t = initTRPC.create();
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';
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