Custom error codes in tRPC
tRPC has a fairly comprehensive set of built-in error codes, but because they’re baked in to the library they are—by definition—not tailored to your application’s specific use case. In AdmitYogi Essays, for instance, we wanted to gate some API operations behind a paywall and we wanted to display some special upsell UI whenever a user hit this error.
We could have overloaded the meaning of the FORBIDDEN
or PRECONDITION_FAILED
error codes, but the developer experience wouldn’t be ideal. Instead, we added a custom PAYWALLED
error code which better communicated the semantic intent.
I’m currently working on building a B2B SaaS where users are assigned to an organization. If a user signs in without having been added to an organization, I want to throw an error and redirect them to an onboarding screen. To do this I need a custom error code, and in this post I’ll explain how to do that.
How to add a custom tRPC error code
First off, you’ll want to define a union type that will be used as your error code type.
import type { TRPC_ERROR_CODE_KEY } from '@trpc/server/rpc'; export type ErrorCode = | TRPC_ERROR_CODE_KEY | 'NO_ORGANIZATION';
After this, define an Error
subclass for each value inside the ErrorCode
type. Start throwing this error when applicable:
// file: server/errors.tsexport class NoOrganizationError extends Error {} // file: server/routers/organization.tsexport const orgRouter = router({ listUsers: procedure.query(async ({ ctx }) => { if (!ctx.auth.orgId) { throw new NoOrganizationError(); } // ... }),});
If you hit this endpoint and trigger the error, you'll see that the custom error code still isn't being returned to your frontend. This is because we need to tell tRPC to return our custom NO_ORGANIZATION
error code when it encounters a thrown NoOrganizationError
. This can be done using tRPC's errorFormatter
API:
import type { TRPC_ERROR_CODE_KEY } from '@trpc/server/rpc'; import type { ErrorCode } from '@/types';import * as errors from '@/server/errors'; function getErrorCode( error: Error | undefined, defaultCode: TRPC_ERROR_CODE_KEY,): ErrorCode { if (error instanceof errors.NoOrganizationError) { return 'NO_ORGANIZATION'; } return defaultCode;} const t = initTRPC.context<Context>().create({ // ... errorFormatter({ error, shape }) { return { ...shape, data: { ...shape.data, code: getErrorCode(error.cause ?? error, error.code), }, }; },});
That’s it! tRPC infers your error type from the return type of your errorFormatter
function, and this inferred type propagates all the way through to your frontend. You’ll now see your custom error code when calling API operations, fully type-safe:
// file: app/team/members/page.tsx'use client'; export function TeamMembersPage() { const router = useRouter(); const { error, ... } = trpc.org.listUsers.useQuery(); // `NO_ORGANIZATION` gets autocompleted here. // If you typo `NO_ORGANIZATON` you'll get a type error. if (error?.data?.code === 'NO_ORGANIZATION') { router.push('/onboard'); } // ...}
Boom! You now have much clearer and specific error codes, which means your error handling logic will end up far more readable and maintainable. As your application grows, you’ll get compounding benefits from having implemented this early.