Error Handling
On this page
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:
{
"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 is true.
initTRPC.create() sets isDev to process.env.NODE_ENV !== 'production' by default.
If you need deterministic behavior across runtimes, override isDev manually.
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
const const t: TRPCRootObject<object, object, {
isDev: false;
}, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t = const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.create<{
isDev: false;
}>(opts?: {
isDev: false;
} | undefined): TRPCRootObject<object, object, {
isDev: false;
}, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
create({ isDev: falseisDev: false });If you need stricter control over which error fields are returned, use 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.
A safe setup has two parts:
onErrorreceives the full error. Log it or send it to your error tracker there.errorFormattercontrols what the client sees. Replace the message ofINTERNAL_SERVER_ERRORs and only return the fields you want to expose.
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
const const t: TRPCRootObject<object, object, {
errorFormatter({ shape }: ErrorFormatterOptions<object | undefined>): {
message: string;
code: TRPC_ERROR_CODE_NUMBER;
data: {
code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST";
httpStatus: number;
path: string | undefined;
};
};
}, {
...;
}>
t = const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.create<{
errorFormatter({ shape }: ErrorFormatterOptions<object | undefined>): {
message: string;
code: TRPC_ERROR_CODE_NUMBER;
data: {
code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST";
httpStatus: number;
path: string | undefined;
};
};
}>(opts?: {
errorFormatter({ shape }: ErrorFormatterOptions<object | undefined>): {
message: string;
code: TRPC_ERROR_CODE_NUMBER;
data: {
code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST";
httpStatus: number;
path: string | undefined;
};
};
} | undefined): TRPCRootObject<...>
create({
function errorFormatter({ shape }: ErrorFormatterOptions<object | undefined>): {
message: string;
code: TRPC_ERROR_CODE_NUMBER;
data: {
code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST";
httpStatus: number;
path: string | undefined;
};
}
errorFormatter({ shape: DefaultErrorShapeshape }) {
return {
message: stringmessage:
shape: DefaultErrorShapeshape.TRPCErrorShape<DefaultErrorData>.data: DefaultErrorDatadata.code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST"code === 'INTERNAL_SERVER_ERROR'
? 'Internal server error'
: shape: DefaultErrorShapeshape.DefaultErrorShape.message: stringmessage,
code: TRPC_ERROR_CODE_NUMBERcode: shape: DefaultErrorShapeshape.DefaultErrorShape.code: TRPC_ERROR_CODE_NUMBERcode,
data: {
code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST";
httpStatus: number;
path: string | undefined;
}
data: {
code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST"code: shape: DefaultErrorShapeshape.TRPCErrorShape<DefaultErrorData>.data: DefaultErrorDatadata.code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST"code,
httpStatus: numberhttpStatus: shape: DefaultErrorShapeshape.TRPCErrorShape<DefaultErrorData>.data: DefaultErrorDatadata.httpStatus: numberhttpStatus,
path: string | undefinedpath: shape: DefaultErrorShapeshape.TRPCErrorShape<DefaultErrorData>.data: DefaultErrorDatadata.path?: string | undefinedpath,
},
};
},
});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.
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. | 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:
import { function getHTTPStatusCodeFromError(error: TRPCError): numbergetHTTPStatusCodeFromError } from '@trpc/server/http';
// Example error you might get if your input validation fails
const const error: TRPCErrorerror: class TRPCErrorTRPCError = {
Error.name: stringname: 'TRPCError',
TRPCError.code: "BAD_GATEWAY" | "BAD_REQUEST" | "CLIENT_CLOSED_REQUEST" | "CONFLICT" | "FORBIDDEN" | "GATEWAY_TIMEOUT" | "INTERNAL_SERVER_ERROR" | "METHOD_NOT_SUPPORTED" | "NOT_FOUND" | "NOT_IMPLEMENTED" | "PARSE_ERROR" | "PAYLOAD_TOO_LARGE" | "PAYMENT_REQUIRED" | "PRECONDITION_FAILED" | "PRECONDITION_REQUIRED" | "SERVICE_UNAVAILABLE" | "TIMEOUT" | "TOO_MANY_REQUESTS" | "UNAUTHORIZED" | "UNPROCESSABLE_CONTENT" | "UNSUPPORTED_MEDIA_TYPE"code: 'BAD_REQUEST',
Error.message: stringmessage: '"password" must be at least 4 characters',
};
if (const error: TRPCErrorerror instanceof class TRPCErrorTRPCError) {
const const httpCode: numberhttpCode = function getHTTPStatusCodeFromError(error: TRPCError): numbergetHTTPStatusCodeFromError(const error: TRPCErrorerror);
var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log(const httpCode: numberhttpCode); // 400
}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:
import { const initTRPC: TRPCBuilder<object, object>initTRPC, class TRPCErrorTRPCError } from '@trpc/server';
const const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t = const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.create<RuntimeConfigOptions<object, object>>(opts?: RuntimeConfigOptions<object, object> | undefined): TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
create();
const const theError: ErrortheError = new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error('something went wrong');
const const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
hello: QueryProcedure<{
input: void;
output: never;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
appRouter = const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{
hello: QueryProcedure<{
input: void;
output: never;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>(_: {
hello: QueryProcedure<{
input: void;
output: never;
meta: object;
errorShape: DefaultErrorShape;
}>;
}) => BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
hello: QueryProcedure<{
input: void;
output: never;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
router({
hello: QueryProcedure<{
input: void;
output: never;
meta: object;
errorShape: DefaultErrorShape;
}>
hello: const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure.ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<never>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, never>): QueryProcedure<{
input: void;
output: never;
meta: object;
errorShape: DefaultErrorShape;
}>
query(() => {
throw new new TRPCError(opts: {
message?: string;
code: TRPC_ERROR_CODE_KEY;
cause?: unknown;
}): TRPCError
TRPCError({
code: "INTERNAL_SERVER_ERROR" | "PARSE_ERROR" | "BAD_REQUEST" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST"code: 'INTERNAL_SERVER_ERROR',
message?: string | undefinedmessage: 'An unexpected error occurred, please try again later.',
// optional: pass the original error to retain stack trace
cause?: unknowncause: const theError: ErrortheError,
});
}),
});
// [...]Results in the following response:
{
"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.
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).
import { function createHTTPServer<TRouter extends AnyRouter>(opts: CreateHTTPHandlerOptions<TRouter>): Server<typeof IncomingMessage, typeof ServerResponse>createHTTPServer } from '@trpc/server/adapters/standalone';
import { const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter } from './router';
const const server: Server<typeof IncomingMessage, typeof ServerResponse>server = createHTTPServer<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateHTTPHandlerOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): Server<typeof IncomingMessage, typeof ServerResponse>
createHTTPServer({
BaseHandlerOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>, IncomingMessage>.router: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
router: const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter,
BaseHandlerOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>, IncomingMessage>.onError?: HTTPErrorHandler<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, IncomingMessage> | undefined
onError(opts: HTTPErrorHandlerOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, IncomingMessage>
opts) {
const { const error: TRPCErrorerror, const type: "query" | "mutation" | "subscription" | "unknown"type, const path: string | undefinedpath, const input: unknowninput, const ctx: object | undefinedctx, const req: IncomingMessagereq } = opts: HTTPErrorHandlerOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, IncomingMessage>
opts;
var console: Consoleconsole.Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)error('Error:', const error: TRPCErrorerror);
if (const error: TRPCErrorerror.TRPCError.code: "BAD_GATEWAY" | "BAD_REQUEST" | "CLIENT_CLOSED_REQUEST" | "CONFLICT" | "FORBIDDEN" | "GATEWAY_TIMEOUT" | "INTERNAL_SERVER_ERROR" | "METHOD_NOT_SUPPORTED" | "NOT_FOUND" | "NOT_IMPLEMENTED" | "PARSE_ERROR" | "PAYLOAD_TOO_LARGE" | "PAYMENT_REQUIRED" | "PRECONDITION_FAILED" | "PRECONDITION_REQUIRED" | "SERVICE_UNAVAILABLE" | "TIMEOUT" | "TOO_MANY_REQUESTS" | "UNAUTHORIZED" | "UNPROCESSABLE_CONTENT" | "UNSUPPORTED_MEDIA_TYPE"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:
interface OnErrorOpts {
OnErrorOpts.error: TRPCErrorerror: class TRPCErrorTRPCError;
OnErrorOpts.type: "query" | "mutation" | "subscription" | "unknown"type: 'query' | 'mutation' | 'subscription' | 'unknown';
OnErrorOpts.path: string | undefinedpath: string | undefined;
OnErrorOpts.input: unknowninput: unknown;
OnErrorOpts.ctx: unknownctx: unknown;
OnErrorOpts.req: Requestreq: Request;
}