From harness-claude
Throws typed TRPCError in procedures for semantic errors (NOT_FOUND, UNAUTHORIZED, BAD_REQUEST), formats Zod field errors via errorFormatter, handles client onError callbacks.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Throw typed TRPCErrors in procedures and format them consistently for client consumption
Guides tRPC end-to-end typesafe API development with router architecture, procedure design, Zod input validation, middleware chaining via unstable_pipe, TRPCError handling, and Vertical Slice pattern.
Builds end-to-end type-safe tRPC APIs with routers, procedures, middleware, subscriptions, and Next.js/React integration for TypeScript full-stack apps.
Defines type-safe inputs and outputs for tRPC procedures using Zod schemas, enabling end-to-end TypeScript inference, runtime validation, and client-server schema sharing.
Share bugs, ideas, or general feedback.
Throw typed TRPCErrors in procedures and format them consistently for client consumption
onError callbacks or error UInew TRPCError({ code: 'NOT_FOUND', message: '...' }) for expected error conditions — it maps to the appropriate HTTP status.code: 'UNAUTHORIZED' for unauthenticated requests and code: 'FORBIDDEN' for insufficient permissions.code: 'BAD_REQUEST' for input that passes Zod schema validation but fails business rules.code: 'UNPROCESSABLE_CONTENT' for field-level validation errors from Zod — pass the ZodError as cause.errorFormatter to initTRPC.create({ errorFormatter }) to shape error responses and extract Zod validation details.onError callbacks of useMutation — check error.data?.code for the tRPC error code.INTERNAL_SERVER_ERROR responses — sanitize in the error formatter.// server/trpc.ts — error formatter with Zod details
import { initTRPC, TRPCError } from '@trpc/server';
import { ZodError } from 'zod';
import superjson from 'superjson';
const t = initTRPC.context<TRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
// server/routers/posts.ts — throwing typed errors
import { TRPCError } from '@trpc/server';
const postsRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string().cuid() }))
.query(async ({ ctx, input }) => {
const post = await ctx.db.post.findUnique({ where: { id: input.id } });
if (!post) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `Post ${input.id} not found`,
});
}
if (post.status === 'draft' && ctx.session?.user.id !== post.authorId) {
throw new TRPCError({ code: 'FORBIDDEN', message: 'Draft not accessible' });
}
return post;
}),
publish: protectedProcedure
.input(z.object({ id: z.string().cuid() }))
.mutation(async ({ ctx, input }) => {
const post = await ctx.db.post.findUnique({ where: { id: input.id } });
if (!post) throw new TRPCError({ code: 'NOT_FOUND' });
if (post.authorId !== ctx.user.id) throw new TRPCError({ code: 'FORBIDDEN' });
if (post.status === 'published') {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Already published' });
}
return ctx.db.post.update({ where: { id: input.id }, data: { status: 'published' } });
}),
});
// Client error handling
const { mutate } = api.posts.publish.useMutation({
onError: (error) => {
if (error.data?.code === 'FORBIDDEN') {
toast.error('You do not have permission to publish this post');
} else if (error.data?.zodError) {
// Field-level errors from errorFormatter
setFieldErrors(error.data.zodError.fieldErrors);
} else {
toast.error(error.message);
}
},
});
tRPC error codes map to HTTP status codes. The mapping is deterministic and built in:
| tRPC code | HTTP status |
|---|---|
BAD_REQUEST | 400 |
UNAUTHORIZED | 401 |
FORBIDDEN | 403 |
NOT_FOUND | 404 |
CONFLICT | 409 |
PRECONDITION_FAILED | 412 |
UNPROCESSABLE_CONTENT | 422 |
TOO_MANY_REQUESTS | 429 |
INTERNAL_SERVER_ERROR | 500 |
Error formatter: The errorFormatter function runs server-side after an error is thrown. It receives the default shape (code, message, data) and can augment it. The example above extracts ZodError.flatten() details into data.zodError so the client can display field-specific error messages.
cause for wrapping: Pass the original error as cause when wrapping: new TRPCError({ code: 'INTERNAL_SERVER_ERROR', cause: dbError }). The cause is accessible in errorFormatter for logging but is not sent to the client.
Client-side error.data: On the client, error.data contains the formatted server response (including zodError if you added it). error.message is the human-readable message. error.data?.code is the tRPC error code string.
onError on the router level: Configure a global onError in the tRPC HTTP adapter to log all procedure errors server-side. This is separate from the errorFormatter — onError is for side effects (logging to Sentry, Datadog), errorFormatter is for shaping the response.
https://trpc.io/docs/server/error-handling