trpc/trpc · error · TRPCError

BAD_REQUEST

BAD_REQUEST

Error message

Invalid input

What it means

This is the generic catch-all inside the content-type `memo().read()` used for input parsing. Any non-TRPCError thrown while reading or deserializing input (e.g. `req.json()` failing, or the transformer's `deserialize` throwing) is rethrown as BAD_REQUEST with the original cause preserved; existing TRPCErrors pass through unchanged.

Source

Thrown at packages/server/src/unstable-core-do-not-import/http/contentType.ts:57

function memo<TReturn>(fn: () => Promise<TReturn>) {
  let promise: Promise<TReturn> | null = null;
  const sym = Symbol.for('@trpc/server/http/memo');
  let value: TReturn | typeof sym = sym;
  return {
    /**
     * Lazily read the value
     */
    read: async (): Promise<TReturn> => {
      if (value !== sym) {
        return value;
      }

      // dedupes promises and catches errors
      promise ??= fn().catch((cause) => {
        if (cause instanceof TRPCError) {
          throw cause;
        }
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: cause instanceof Error ? cause.message : 'Invalid input',
          cause,
        });
      });

      value = await promise;
      promise = null;

      return value;
    },
    /**
     * Get an already stored result
     */
    result: (): TReturn | undefined => {
      return value !== sym ? value : undefined;
    },
  };

View on GitHub (pinned to acff82332d)

Solutions

  1. Inspect `error.cause` (and `error.data.cause`) for the real underlying reason.
  2. Ensure transformer type registration matches exactly on client and server.
  3. Validate the payload shape on the client before sending, and surface transformer errors clearly during development.

Example fix

// before - superjson custom type registered only on client
// client:  superjson.registerCustom(MyDate, 'date')
// server:  (missing)

// after - register on BOTH sides, shared module
// shared/registry.ts
export const s = SuperJSON;
s.registerCustom(Date, (v) => v.toISOString(), (v) => new Date(v));
// import { s } from '../shared/registry' on both client and server
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate the body parses with the configured transformer before sending
import { transformer } from '../shared/transformer';
function safeSerialize(value: unknown): string {
  const wire = transformer.input.serialize(value);
  return JSON.stringify(wire); // throws here if the type is unregistered
}

Type guard

function isTransformerRegistered(
  transformer: { input: { serialize: (x: unknown) => unknown } },
  sample: unknown,
): boolean {
  try { transformer.input.serialize(sample); return true; } catch { return false; }
}

Try / catch

try {
  await trpc.proc.query(input);
} catch (e) {
  if (e instanceof TRPCError && e.code === 'BAD_REQUEST' && /Invalid input/.test(e.message)) {
    // Inspect e.cause: usually JSON.parse or transformer.deserialize failed
  }
  throw e;
}

Prevention

When it happens

Trigger: A malformed JSON body; a superjson transformer failing to deserialize a value whose custom type is not registered; a custom transformer throwing on unexpected input.

Common situations: Client forgot to register a custom Superjson type on the server; client sent values like unregistered dates/classes; transformer version skew between client and server.

Related errors


AI-assisted analysis of trpc/trpc@acff82332d (2026-08-12). Data as JSON: /api/errors/0411a04449a7e99f. Report an issue: GitHub.