DEV Community

Sophia Willows
Sophia Willows

Posted on • Originally published at sophiabits.com

Return 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';
Enter fullscreen mode Exit fullscreen mode

After this, define an Error subclass for each value inside the ErrorCode type. Start throwing this error when applicable:

// file: server/errors.ts
export class NoOrganizationError extends Error {}

// file: server/routers/organization.ts
export const orgRouter = router({
  listUsers: procedure.query(async ({ ctx }) => {
    if (!ctx.auth.orgId) {
      throw new NoOrganizationError();
    }

    // ...
  }),
});
Enter fullscreen mode Exit fullscreen mode

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),
      },
    };
  },
});
Enter fullscreen mode Exit fullscreen mode

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

  // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)