Skip to content

@trpc/server

The tRPC server library

error-handling

360 linesSource

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.

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 is true. initTRPC.create() sets isDev to process.env.NODE_ENV !== 'production' by default. If you need deterministic behavior across runtimes, override isDev manually.

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.

Error codes

tRPC defines a list of error codes that each represent a different type of error and response with a different HTTP code.

CodeDescriptionHTTP code
PARSE_ERRORInvalid JSON was received by the server, or an error occurred while parsing the request.400
BAD_REQUESTThe server cannot or will not process the request due to something that is perceived to be a client error.400
UNAUTHORIZEDThe client request has not been completed because it lacks valid authentication credentials for the requested resource.401
PAYMENT_REQUIREDThe client request requires payment to access the requested resource.402
FORBIDDENThe client is not authorized to access the requested resource.403
NOT_FOUNDThe server cannot find the requested resource.404
METHOD_NOT_SUPPORTEDThe server knows the request method, but the target resource doesn't support this method.405
TIMEOUTThe server would like to shut down this unused connection.408
CONFLICTThe request conflicts with the current state of the target resource.409
PRECONDITION_FAILEDAccess to the target resource has been denied.412
PAYLOAD_TOO_LARGERequest entity is larger than limits defined by server.413
UNSUPPORTED_MEDIA_TYPEThe server refuses to accept the request because the payload format is in an unsupported format.415
UNPROCESSABLE_CONTENTThe server understands the request method, and the request entity is correct, but the server was unable to process it.422
PRECONDITION_REQUIREDThe 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_REQUESTSThe rate limit has been exceeded or too many requests are being sent to the server.429
CLIENT_CLOSED_REQUESTThe client closed the connection before the server finished responding.499
INTERNAL_SERVER_ERRORAn unspecified error occurred.500
NOT_IMPLEMENTEDThe server does not support the functionality required to fulfill the request.501
BAD_GATEWAYThe server received an invalid response from the upstream server.502
SERVICE_UNAVAILABLEThe server is not ready to handle the request.503
GATEWAY_TIMEOUTThe 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
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.

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:

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.

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).

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
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

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

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_ERRORs:

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 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.

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

ts
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
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;
}
  • server-setup (https://trpc.io/docs/server/overview): npx @tanstack/intent@latest load @trpc/server#server-setup