@trpc/server
Version:
254 lines (199 loc) • 6.33 kB
Markdown
---
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.15.1'
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
```ts
// 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
```ts
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
```ts
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
```ts
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
```ts
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:
```ts
import { publicProcedure } from './trpc';
const proc = publicProcedure.query(() => {
throw new Error('Not found');
// client receives 500 INTERNAL_SERVER_ERROR
});
```
Correct:
```ts
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:
```ts
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:
```ts
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:
```ts
import { initTRPC } from '@trpc/server';
// No errorFormatter -- client gets generic "Input validation failed"
const t = initTRPC.create();
```
Correct:
```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,
},
};
},
});
```
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