561 words
3 minutes
My effect trpc helper utils

I want to start sharing my journey with Effect and how I apply it. Some of solutions I share may not be the best, but I strive to learn and continue to improve as I go! I started with no knowledge of Effect and now it’s my go-to, it’s a joy to work with.

Effect and tRPC#

I opted to use tRPC with Effect for the sole reason that Effect RPC was a bit immature when I decided to pick-up Effect on my existing tRPC project.

Effect generic helper utilities for my tRPC Presentation layer#

The goal here is to provide a consistent set of utilities for my tRPC Presentation layer allowing the frontend to receive the correct TRPCError in the event of an error.

handleApplicationErrors#

A generic handler for common errors from my Application layer.

Instead of re-creating the Effect.catchTags and handling the same errors over and over again, I just made a utility that I can re-use which will ensure the correct TRPCError is returned in my handleEffectExit. This ensures consistency for the frontend and the errors it receives.

export const handleApplicationErrors = <R, E, A>(
  effect: Effect.Effect<R, E, A>,
) =>
  effect.pipe(
    Effect.catchTags({
      InsufficientPermission: () =>
        new TRPCErrorEff({
          trpcError: new TRPCError({
            code: "UNAUTHORIZED",
            message: "Not authorized",
          }),
        }),
      InsufficentRoles: () =>
        new TRPCErrorEff({
          trpcError: new TRPCError({
            code: "FORBIDDEN",
            message: "Insufficent permissions",
          }),
        }),
      FileTooLarge: () =>
        new TRPCErrorEff({
          trpcError: new TRPCError({
            code: "BAD_REQUEST",
            message: "File is too large",
          }),
        }),
    }),
  );

Usage#

// Now my `handleEffectExit` can ensure a TRPC error is returned when an `Effect` fails
yield* handleApplicationErrors(effect);

handleEffectExit#

The following function is used to ensure a TRPC error is returned when an Effect fails.

export const handleEffectExit = <E, A>(exit: Exit.Exit<A, E>): A => {
  if (Exit.isSuccess(exit)) {
    return exit.value;
  } else {
    throw Cause.match(exit.cause, {
      onFail(error) {
        if (error instanceof TRPCErrorEff) {
          return error.trpcError as unknown;
        }
        
        console.error("handleEffectExit: Not a TRPCErrorEff, returning generic something went wrong error", error);
        return new TRPCError({
          code: "INTERNAL_SERVER_ERROR",
          message: "Something went wrong.",
        }) as unknown;
      },
      onDie(defect) {
        if (defect instanceof TRPCErrorEff) {
          return defect.trpcError as unknown;
        }

        console.error("handleEffectExit: Not a TRPCErrorEff, returning generic something went wrong error", defect);
        return new TRPCError({
          code: "INTERNAL_SERVER_ERROR",
          message: "Something went wrong",
        });
      },
      onInterrupt(fiberId) {
        console.error("handleEffectExit: onInterrupt", fiberId);
        return new TRPCError({
          code: "INTERNAL_SERVER_ERROR",
          message: "Something went wrong",
        });
      },
      onParallel(left, right) {
        console.error("handleEffectExit: onParallel", left, right);
        return new TRPCError({
          code: "INTERNAL_SERVER_ERROR",
          message: "Something went wrong",
        });
      },
      onEmpty() {
        console.error("handleEffectExit: onEmpty");
        return new TRPCError({
          code: "INTERNAL_SERVER_ERROR",
          message: "Something went wrong",
        });
      },
      onSequential() {
        console.error("handleEffectExit: onSequential");

        return new TRPCError({
          code: "INTERNAL_SERVER_ERROR",
          message: "Something went wrong",
        });
      },
    });
  }
};

Usage with tRPC#

Here is how I use these in my tRPC procedure. I have only recently got around to thinking about this layer of my application, so I am sure there is some improvements here I can make. But this defintely helps reduce the amount of boilerplate I have to write.

export const acceptFriendRequest = protectedProcedure
  .input(
    // My input validation here
  )
  // My effect ctx passes around the Effect runtime I use
  .mutation(async ({ ctx, input }) => {
    return ctx.effectRuntime.runPromiseExit(
      // My effect here
      Effect.gen(function()* { 
        return yield* handleApplicationErrors(
          // Example - This will throw an error
          Effect.gen(function()* { 
            yield* new InsufficientPermission();
          })
        )
        // I can still .pipe() here and handle errors specific to the effect
      })
    )
    // Ensure that errors are handled appropriately & return success/failure to the frontend
    .then(handleEffectExit)
  })
My effect trpc helper utils
https://personal-blog-flame-phi.vercel.app/posts/effect-trpc-helper-utils/
Author
James Wainwright
Published at
2025-02-16