Skip to content
tRPC

Partners

Error Formatting

On this page

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

interface ErrorFormatterOpts {
  error: TRPCError;
  type: 'query' | 'mutation' | 'subscription' | 'unknown';
  path: string | undefined;
  input: unknown;
  ctx: unknown;
  shape: { message: string; code: number; data: unknown };
}

DefaultErrorShape:

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