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