@trpc/server
Version:
397 lines (315 loc) • 18.8 kB
Markdown
---
name: error-handling
description: Throw typed errors with TRPCError and error codes (NOT_FOUND, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST, INTERNAL_SERVER_ERROR), configure errorFormatter for client-side Zod error display or to hide internal error messages, format errors per procedure with .errors(), handle errors globally with the adapter's onError callback, map tRPC errors to HTTP status codes with getHTTPStatusCodeFromError(), control stack traces with isDev.
requires:
- server-setup
sources:
- www/v11/docs/server/error-handling.md
- www/v11/docs/server/error-formatting.md
metadata:
type: core
library: trpc
library_version: 11.25.0
---
<!-- Generated from www/v11/docs/server/error-handling.md by www/build/skills.ts. Edit the docs instead. -->
# Error Handling
From https://trpc.io/docs/server/error-handling
Whenever an error occurs in a procedure, tRPC responds to the client with an object that includes an "error" property. This property contains all the information that you need to handle the error in the client.
Here's an example error response caused by a bad request input:
```json
{
"id": null,
"error": {
"message": "\"password\" must be at least 4 characters",
"code": -32600,
"data": {
"code": "BAD_REQUEST",
"httpStatus": 400,
"stack": "...",
"path": "user.changepassword"
}
}
}
```
## Stack traces in production
By default, tRPC includes `error.data.stack` only when [`isDev`](https://trpc.io/docs/server/routers#initialize-trpc) is `true`.
`initTRPC.create()` sets `isDev` to `process.env.NODE_ENV !== 'production'` by default.
If you need deterministic behavior across runtimes, override `isDev` manually.
```ts no-check title="server.ts"
import { initTRPC } from '@trpc/server';
const t = initTRPC.create({ isDev: false });
```
If you need stricter control over which error fields are returned, use [error formatting](https://trpc.io/docs/server/error-formatting).
## Secure error reporting
Errors that are not a `TRPCError` are wrapped as `INTERNAL_SERVER_ERROR` and keep the original `message`. A database driver or a library can put table names, queries or file paths in that message, and it would reach the client. This is the kind of information leak [OWASP describes](https://owasp.org/www-community/Improper_Error_Handling).
A safe setup has two parts:
- [`onError`](https://trpc.io/docs/server/error-handling#handling-errors) receives the full error. Log it or send it to your error tracker there.
- [`errorFormatter`](https://github.com/trpc/trpc/blob/main/www/v11/docs/server/error-formatting) controls what the client sees. Replace the message of `INTERNAL_SERVER_ERROR`s and only return the fields you want to expose.
```ts no-check title="server.ts"
import { initTRPC } from '@trpc/server';
const t = initTRPC.create({
errorFormatter({ shape }) {
return {
message:
shape.data.code === 'INTERNAL_SERVER_ERROR'
? 'Internal server error'
: shape.message,
code: shape.code,
data: {
code: shape.data.code,
httpStatus: shape.data.httpStatus,
path: shape.data.path,
},
};
},
});
```
The formatter above keeps the message of every other code. A `TRPCError` you throw yourself with a code like `NOT_FOUND` or `UNAUTHORIZED` is shown as written, so write messages that are safe to show to users. One thrown with `INTERNAL_SERVER_ERROR` is replaced too.
> [!NOTE]
>
> Input validation errors are `BAD_REQUEST`s and their message is the output of your validation library. If you don't want to reveal which library you use or how your input is shaped, replace that message in the `errorFormatter` as well.
## Error codes
tRPC defines a list of error codes that each represent a different type of error and response with a different HTTP code.
| Code | Description | HTTP code |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- |
| PARSE_ERROR | Invalid JSON was received by the server, or an error occurred while parsing the request. | 400 |
| BAD_REQUEST | The server cannot or will not process the request due to something that is perceived to be a client error. | 400 |
| UNAUTHORIZED | The client request has not been completed because it lacks valid authentication credentials for the requested resource. | 401 |
| PAYMENT_REQUIRED | The client request requires payment to access the requested resource. | 402 |
| FORBIDDEN | The client is not authorized to access the requested resource. | 403 |
| NOT_FOUND | The server cannot find the requested resource. | 404 |
| METHOD_NOT_SUPPORTED | The server knows the request method, but the target resource doesn't support this method. | 405 |
| TIMEOUT | The server would like to shut down this unused connection. | 408 |
| CONFLICT | The request conflicts with the current state of the target resource. | 409 |
| PRECONDITION_FAILED | Access to the target resource has been denied. | 412 |
| PAYLOAD_TOO_LARGE | Request entity is larger than limits defined by server. | 413 |
| UNSUPPORTED_MEDIA_TYPE | The server refuses to accept the request because the payload format is in an unsupported format. | 415 |
| UNPROCESSABLE_CONTENT | The server understands the request method, and the request entity is correct, but the server was unable to process it. | 422 |
| PRECONDITION_REQUIRED | [The server cannot process the request because a required precondition header (such as `If-Match`) is missing. When a precondition header does not match the server-side state, the response should be `412 Precondition Failed`.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428) | 428 |
| TOO_MANY_REQUESTS | The rate limit has been exceeded or too many requests are being sent to the server. | 429 |
| CLIENT_CLOSED_REQUEST | The client closed the connection before the server finished responding. | 499 |
| INTERNAL_SERVER_ERROR | An unspecified error occurred. | 500 |
| NOT_IMPLEMENTED | The server does not support the functionality required to fulfill the request. | 501 |
| BAD_GATEWAY | The server received an invalid response from the upstream server. | 502 |
| SERVICE_UNAVAILABLE | The server is not ready to handle the request. | 503 |
| GATEWAY_TIMEOUT | The server did not get a response in time from the upstream server that it needed in order to complete the request. | 504 |
tRPC exposes a helper function, `getHTTPStatusCodeFromError`, to help you extract the HTTP code from the error:
```ts no-check
import { getHTTPStatusCodeFromError } from '@trpc/server/http';
// Example error you might get if your input validation fails
const error: TRPCError = {
name: 'TRPCError',
code: 'BAD_REQUEST',
message: '"password" must be at least 4 characters',
};
if (error instanceof TRPCError) {
const httpCode = getHTTPStatusCodeFromError(error);
console.log(httpCode); // 400
}
```
> [!TIP]
>
> There's a full example of how error handling works in a server-side context in the [Server Side Calls docs](https://trpc.io/docs/server/server-side-calls).
## Throwing errors
tRPC provides an error subclass, `TRPCError`, which you can use to represent an error that occurred inside a procedure.
For example, throwing this error:
```ts no-check title="server.ts"
import { initTRPC, TRPCError } from '@trpc/server';
const t = initTRPC.create();
const theError = new Error('something went wrong');
const appRouter = t.router({
hello: t.procedure.query(() => {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred, please try again later.',
// optional: pass the original error to retain stack trace
cause: theError,
});
}),
});
// [...]
```
Results in the following response:
```json
{
"id": null,
"error": {
"message": "An unexpected error occurred, please try again later.",
"code": -32603,
"data": {
"code": "INTERNAL_SERVER_ERROR",
"httpStatus": 500,
"stack": "...",
"path": "hello"
}
}
}
```
Throw a `TRPCError` with the code that fits, like `NOT_FOUND` or `FORBIDDEN`, so the client gets the right HTTP status. Any other error thrown in a procedure or middleware becomes an `INTERNAL_SERVER_ERROR` with the original error as its `cause`.
> [!WARNING]
> The `message` of an error you didn't throw as a `TRPCError` is still sent to the client, in production too. If your errors may contain internal details, [hide them in your error formatter](https://trpc.io/docs/server/error-formatting#hiding-internal-error-messages).
## Handling errors
All errors that occur in a procedure go through the `onError` method before being sent to the client. Here you can handle errors (To change errors see [error formatting](https://trpc.io/docs/server/error-formatting)).
```ts no-check title="server.ts"
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './router';
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
}
},
});
```
The `onError` parameter is an object that contains all information about the error and the context it occurs in:
```ts no-check
interface OnErrorOpts {
error: TRPCError;
type: 'query' | 'mutation' | 'subscription' | 'unknown';
path: string | undefined;
input: unknown;
ctx: unknown;
req: Request;
}
```
## Error Formatting
From https://trpc.io/docs/server/error-formatting
The error formatting in your router will be inferred all the way to your client.
### Usage example highlighted
#### Adding custom formatting
```ts no-check title="server.ts"
import { initTRPC } from '@trpc/server';
import { ZodError } from 'zod';
export const t = initTRPC.create({
errorFormatter(opts) {
const { shape, error } = opts;
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
```
#### Usage in React
```tsx no-check title="components/MyComponent.tsx"
import { useEffect } from 'react';
import { trpc } from '../utils/trpc';
export function MyComponent() {
const mutation = trpc.addPost.useMutation();
useEffect(() => {
mutation.mutate({ title: 'example' });
}, []);
if (mutation.error?.data?.zodError) {
// zodError will be inferred
return (
<pre>Error: {JSON.stringify(mutation.error.data.zodError, null, 2)}</pre>
);
}
return <>[...]</>;
}
```
### Hiding internal error messages
The default shape includes the error's `message`, even for unexpected errors such as a failed database query. To keep those details out of production responses, replace the message of `INTERNAL_SERVER_ERROR`s:
```ts no-check title="server.ts"
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create({
errorFormatter(opts) {
const { shape, error } = opts;
if (
error.code === 'INTERNAL_SERVER_ERROR' &&
process.env.NODE_ENV === 'production'
) {
return { ...shape, message: 'Internal server error' };
}
return shape;
},
});
```
The full error, with its `cause`, still reaches your [`onError`](https://trpc.io/docs/server/error-handling#handling-errors) handler.
### Per-procedure error formatting
Errors can be formatted on a per-procedure basis using the `.errors()` method, which can be chained to produce a union of possible error shapes for each procedure.
When a middleware or procedure throws an error, it bubbles upward through all `.errors()` handlers until one returns a shape or the global errorFormatter is reached.
```ts no-check title="server.ts"
const rateLimitedProcedure = t.procedure
.errors((opts) => {
if (opts.error.cause instanceof RateLimitError) {
return {
...opts.shape,
data: { ...opts.shape.data, kind: 'RATE_LIMIT' as const },
};
}
return undefined;
})
.use(opts => {
// Important: errors bubble up so a throwing middleware must come after the .errors() handler
if (isRateLimited(opts)) {
throw new RateLimitError();
}
return opts.next();
});
const billedProcedure = rateLimitedProcedure
.errors((opts) => {
if (opts.error.cause instanceof PaymentRequiredError) {
return {
...opts.shape,
data: {
...opts.shape.data,
kind: 'PAYMENT_REQUIRED' as const,
amountDue: opts.error.cause.amountDue,
},
};
}
return undefined;
});
const addPostProcedure = billedProcedure.mutation(opts => {
if (isPaymentRequired(opts)) {
throw new PaymentRequiredError(100);
}
return {
success: true,
};
});
```
### All properties sent to `errorFormatter()`
> tRPC is compliant with [JSON-RPC 2.0](https://www.jsonrpc.org/specification)
```ts no-check
interface ErrorFormatterOpts {
error: TRPCError;
type: 'query' | 'mutation' | 'subscription' | 'unknown';
path: string | undefined;
input: unknown;
ctx: unknown;
shape: { message: string; code: number; data: unknown };
}
```
**`DefaultErrorShape`:**
```ts no-check
type DefaultErrorData = {
code: TRPC_ERROR_CODE_KEY;
httpStatus: number;
/**
* Path to the procedure that threw the error
*/
path?: string;
/**
* Stack trace of the error (only in development)
*/
stack?: string;
};
interface DefaultErrorShape {
message: string;
code: TRPC_ERROR_CODE_NUMBER;
data: DefaultErrorData;
}
```
## Related skills
- `server-setup` (https://trpc.io/docs/server/overview): `npx @tanstack/intent@latest load @trpc/server#server-setup`