trpc

How to catch errors inside tRPC Middleware thrown in tRPC procedures?


I'm working on a tRPC application integrating Google Drive and Spreadsheet APIs. To facilitate this, I've implemented a custom googleProcedure middleware. This middleware injects necessary context into queries or mutations, enabling access to Drive files or Spreadsheets based on the authenticated user.

My goal is to streamline error handling. Specifically, I want to intercept errors emanating from queries or procedures connected to Google APIs. I aim to avoid the redundancy of catching errors in each individual procedure. However, my current implementation doesn't effectively capture errors within the procedures. Here's the problematic snippet:

const googleProcedure = protectedProcedure.use(async ({ ctx, next }) => {
  // Initialization of googleClient, sheets, and drive

  try {
    return await next({
      ctx: {
        ...ctx,
        sheets,
        drive,
      },
    });
  } catch (err) {
    if (err && err instanceof GaxiosError && err.code == '401') {
      throw new TRPCError({
        code: 'UNAUTHORIZED',
        message: 'Not authenticated with Google',
      });
    }
    throw err;
  }
});

// Definitions for googleSpreadsheet router...
export const spreadsheet = router({
  create: googleProcedure.mutation(async ({ ctx, input }) => {}),
  appendRow: googleProcedure.mutation(async ({ ctx, input }) => {}),
  getAll: googleProcedure.query(async ({ ctx, input }) => {}),
  trash: googleProcedure.mutation(async ({ ctx, input }) => {}),
});

In this code, I attempted to catch Google-related 401 errors and rethrow them as a TRPCError with an 'UNAUTHORIZED' code. Unfortunately, this approach doesn't capture errors as expected.

I'm seeking advice on how to effectively catch and handle these errors in the middleware layer, without embedding try-catch blocks in each procedure. Any insights or suggestions would be greatly appreciated!


Solution

  • Ah! awaiting next() always succeeds even if an error is thrown in the procedure. The object returned from calling next({ ctx }) has an ok boolean property and error property if ok is false. So here is the code that allows intercepting errors from inside the middleware. And this actually works as expected and I get an UNAUTHORIZED error when navigating to the test query procedure instead of the internal server error that gets thrown initially.

    const errorHandlingProcedure = publicProcedure.use(async ({ ctx, next }) => {
      const resp = await next({ ctx });
    
      if (!resp.ok) {
        console.log('middleware intercepted error');
        throw new TRPCError({ code: 'UNAUTHORIZED' });
      }
    
      return resp;
    });
    
    export const appRouter = router({
      test: errorHandlingProcedure.query(async () => {
        throw new Error('Error from test procedure');
      }),
    });
    

    So from my initial google integration example, I could check if the resp.error.cause is an instance of the Google Error class and then handle it accordingly.

    import { GaxiosError } from "googleapis-common";
    
    const googleProcedure = protectedProcedure.use(async ({ ctx, next }) => {
      // Initialization of googleClient, sheets, and drive
    
      const resp = await next({
        ctx: {
          ...ctx,
          sheets,
          drive,
        },
      });
    
      if (!resp.ok && resp.error.cause instanceof GaxiosError && resp.error.cause.code === '401') {
        throw new TRPCError({
          code: 'UNAUTHORIZED',
          message: 'Not authenticated with Google',
        });
      }
    
      return resp;
    });