install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/trpc-error-handling" ~/.claude/skills/intense-visions-harness-engineering-trpc-error-handling && rm -rf "$T"
manifest:
agents/skills/claude-code/trpc-error-handling/SKILL.mdsource content
tRPC: Error Handling
Throw typed TRPCErrors in procedures and format them consistently for client consumption
When to Use
- Returning semantic HTTP-equivalent errors from tRPC procedures (404, 401, 422)
- Formatting error responses with additional metadata (field-level validation errors)
- Handling tRPC errors on the client in
callbacks or error UIonError - Logging server-side errors with request context (user ID, input, procedure name)
- Distinguishing between expected errors (validation, not found) and unexpected errors (database failures)
Instructions
- Throw
for expected error conditions — it maps to the appropriate HTTP status.new TRPCError({ code: 'NOT_FOUND', message: '...' }) - Use
for unauthenticated requests andcode: 'UNAUTHORIZED'
for insufficient permissions.code: 'FORBIDDEN' - Use
for input that passes Zod schema validation but fails business rules.code: 'BAD_REQUEST' - Use
for field-level validation errors from Zod — pass thecode: 'UNPROCESSABLE_CONTENT'
asZodError
.cause - Add an
toerrorFormatter
to shape error responses and extract Zod validation details.initTRPC.create({ errorFormatter }) - Handle errors on the client in
callbacks ofonError
— checkuseMutation
for the tRPC error code.error.data?.code - Never expose internal error messages or stack traces in
responses — sanitize in the error formatter.INTERNAL_SERVER_ERROR
// 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); } }, });
Details
tRPC error codes map to HTTP status codes. The mapping is deterministic and built in:
| tRPC code | HTTP status |
|---|---|
| 400 |
| 401 |
| 403 |
| 404 |
| 409 |
| 412 |
| 422 |
| 429 |
| 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.
for wrapping: Pass the original error as cause
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
: On the client, error.data
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.
on the router level: Configure a global onError
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.
Source
https://trpc.io/docs/server/error-handling
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
- related_skills: trpc-router-composition, trpc-input-validation, trpc-react-query-integration, next-error-boundaries, api-error-contracts, api-status-codes
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.