Skip to main content

Error Handling

This guide focuses on advanced patterns for translating Nest errors into consistent tRPC error semantics.

Mental Model

@nest-native/trpc runs through the Nest enhancer pipeline:

  • guards
  • interceptors
  • pipes
  • filters

Any uncaught error is converted to a TRPCError.

Default Mapping

When you throw an HttpException, the package maps known HTTP status codes to tRPC codes:

  • 400 -> BAD_REQUEST
  • 401 -> UNAUTHORIZED
  • 403 -> FORBIDDEN
  • 404 -> NOT_FOUND
  • 409 -> CONFLICT
  • 422 -> UNPROCESSABLE_CONTENT
  • 429 -> TOO_MANY_REQUESTS
  • 503 -> SERVICE_UNAVAILABLE

Unknown statuses fall back to INTERNAL_SERVER_ERROR.

Filter Remapping Pattern

Use a filter when you want to change how a class of errors appears to clients.

import {
BadRequestException,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';

@Catch(BadRequestException)
export class RemapBadRequestFilter implements ExceptionFilter {
catch(_exception: BadRequestException): never {
// Remap 400 to 422 for clients.
throw new HttpException('filtered payload', 422);
}
}
@Mutation()
@UseFilters(RemapBadRequestFilter)
create(@Input() input: CreateUserDto) {
return this.usersService.create(input);
}

Custom Error Shapes with errorFormatter

To change the shape of the error payload sent to clients (rather than which error is thrown), configure errorFormatter on the module. It is forwarded to initTRPC.create({ errorFormatter }) and runs after the HttpExceptionTRPCError mapping above, so error.code is already the mapped tRPC code:

import { z, ZodError } from 'zod';

TrpcModule.forRoot({
errorFormatter: ({ shape, error }) => ({
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? z.flattenError(error.cause)
: null,
},
}),
});

Clients read the extra fields from error.data (e.g. error.data.zodError.fieldErrors). Combine with onError for centralized reporting:

TrpcModule.forRoot({
onError: ({ error, path, type }) => {
logger.error(`tRPC ${type} "${path}" failed: ${error.code}`);
},
});

See sample/13-transformer-error-formatting for the runnable recipe.

Custom Application Codes (Idiomatic Pattern)

tRPC transport codes are fixed. For app-specific codes, keep a stable transport code and add a domain code in the message.

import { Catch, ExceptionFilter } from '@nestjs/common';
import { TRPCError } from '@trpc/server';

class DomainRuleException extends Error {
constructor(
readonly appCode: 'EMAIL_TAKEN' | 'PLAN_LIMIT_REACHED',
message: string,
) {
super(message);
}
}

@Catch(DomainRuleException)
export class DomainRuleFilter implements ExceptionFilter {
catch(exception: DomainRuleException): never {
throw new TRPCError({
code: 'CONFLICT',
message: `[${exception.appCode}] ${exception.message}`,
cause: exception,
});
}
}
if (await this.usersService.emailExists(input.email)) {
throw new DomainRuleException('EMAIL_TAKEN', 'Email already exists');
}

This keeps client logic predictable:

  • transport decision by error.data.code (CONFLICT)
  • domain decision by your app code (EMAIL_TAKEN)

Client Handling Example

try {
await trpc.users.create.mutate(input);
} catch (error: any) {
if (error?.data?.code === 'CONFLICT' && /EMAIL_TAKEN/.test(error.message)) {
// show "email already exists"
}
}

Testing Checklist

Test these explicitly:

  • remapped status -> expected tRPC code
  • message shape from filters
  • domain-code prefix parsing
  • fallback behavior for unmapped errors

Reference tests:

  • packages/trpc/test/context/trpc-context-creator.spec.ts
  • packages/trpc/test/router/trpc-router-lifecycle.spec.ts
  • packages/trpc/test/adapter/trpc-http-adapter.spec.ts (errorFormatter and onError passthrough)