Error Handling
GTM Clarity uses tRPC's structured error system. All errors are returned as typed TRPCError objects with a code, message, and optional cause. This page documents the error codes, common scenarios, and client-side handling patterns.
Error Response Structure
All tRPC errors follow this shape:
{
"error": {
"message": "Not signed in",
"code": -32001,
"data": {
"code": "UNAUTHORIZED",
"httpStatus": 401,
"path": "scoring.getMatrixData"
}
}
}
Error Codes
| tRPC Code | HTTP Status | When It Occurs |
|---|---|---|
UNAUTHORIZED | 401 | No valid Clerk session -- user is not signed in |
FORBIDDEN | 403 | No organization selected, or insufficient role |
NOT_FOUND | 404 | Requested entity does not exist within the tenant |
BAD_REQUEST | 400 | Input validation failed (Zod) or invalid parameters |
INTERNAL_SERVER_ERROR | 500 | Unexpected server error |
UNAUTHORIZED
Returned when the isAuthed middleware cannot find a valid Clerk userId in the session.
// src/server/trpc/init.ts
const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.auth.userId) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not signed in" });
}
return next({ ctx: { ...ctx, userId: ctx.auth.userId } });
});
Common causes:
- Session expired
- User not logged in
- Clerk middleware not configured
Client handling:
import { useQuery } from "@tanstack/react-query";
import { TRPCClientError } from "@trpc/client";
function ScoringPage() {
const trpc = useTRPC();
const { data, error } = useQuery(
trpc.scoring.getMatrixData.queryOptions({ limit: 500 })
);
if (error instanceof TRPCClientError) {
if (error.data?.code === "UNAUTHORIZED") {
return <RedirectToLogin />;
}
}
return <ScoreMatrix data={data} />;
}
FORBIDDEN
Returned in two scenarios:
1. No organization selected -- the hasTenant middleware requires an active org:
if (!ctx.auth.orgId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "No organization selected",
});
}
2. Insufficient role -- the isAdmin middleware checks for org:admin:
if (!ctx.auth.has({ role: "org:admin" })) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Admin access required",
});
}
Common causes:
- User has not selected an organization in Clerk
- User has
org:viewerororg:managerrole but called an admin-only procedure - Organization membership was revoked
Check the message field to distinguish between "no org" and "insufficient role" cases:
"No organization selected"-- prompt the user to select an org"Admin access required"-- show a permission denied message
NOT_FOUND
Returned when a requested resource does not exist or does not belong to the current tenant.
throw new TRPCError({
code: "NOT_FOUND",
message: "Scored entity not found",
});
Procedures that throw NOT_FOUND:
| Router | Procedure | When |
|---|---|---|
scoring | getEntityDetail | Entity type + ID has no score record |
buyingGroup | getDetail | Buying group ID not found in tenant |
buyingGroup | getMembers | Buying group ID not found in tenant |
buyingGroup | getMemberEngagement | Buying group ID not found in tenant |
buyingGroup | getCandidates | Buying group ID not found in tenant |
buyingGroup | confirmMemberRole | Member ID not found in tenant |
fieldMapping | listSourceFields | Connector not found or not connected |
writeback | saveConfig | Connector not found in tenant |
writeback | listWriteableFields | Connector not found or not connected |
BAD_REQUEST
Returned when Zod input validation fails or when business logic rejects the input.
Zod Validation Errors
When a procedure input fails Zod validation, tRPC wraps the Zod issues into a BAD_REQUEST error:
{
"error": {
"message": "[\n {\n \"code\": \"invalid_type\",\n \"expected\": \"string\",\n \"received\": \"undefined\",\n \"path\": [\"entityType\"],\n \"message\": \"Required\"\n }\n]",
"code": -32600,
"data": {
"code": "BAD_REQUEST",
"httpStatus": 400,
"path": "scoring.getEntityDetail"
}
}
}
Client handling for validation errors:
import { TRPCClientError } from "@trpc/client";
function handleError(error: unknown) {
if (error instanceof TRPCClientError) {
if (error.data?.code === "BAD_REQUEST") {
// Parse Zod issues from the message
try {
const issues = JSON.parse(error.message);
const fieldErrors = issues.map(
(i: { path: string[]; message: string }) =>
`${i.path.join(".")}: ${i.message}`
);
return fieldErrors;
} catch {
return [error.message];
}
}
}
return ["An unexpected error occurred"];
}
Business Logic Errors
Some procedures throw BAD_REQUEST for domain-specific validation:
| Router | Procedure | Scenario |
|---|---|---|
fieldMapping | saveMappings | Required canonical fields missing from mappings |
fieldMapping | listSourceFields | Unsupported entity type |
Example:
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid mappings: Missing required field: externalId",
});
INTERNAL_SERVER_ERROR
Returned for unexpected failures such as database errors, network timeouts, or unhandled exceptions. The actual error details are logged server-side but not exposed to the client.
Client handling:
if (error instanceof TRPCClientError) {
if (error.data?.code === "INTERNAL_SERVER_ERROR") {
return <ErrorPage message="Something went wrong. Please try again." />;
}
}
Never expose internal error details (stack traces, SQL errors) to the client. The tRPC layer ensures only the code and a generic message are returned.
Global Error Handling Pattern
Set up a global error handler using TanStack Query's QueryClient configuration:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
if (error instanceof TRPCClientError) {
// Don't retry auth or validation errors
const code = error.data?.code;
if (code === "UNAUTHORIZED" || code === "FORBIDDEN" || code === "BAD_REQUEST") {
return false;
}
}
return failureCount < 3;
},
},
mutations: {
retry: false,
},
},
});
- Queries retry up to 3 times for transient errors (network, 500s)
- Mutations should not retry automatically to avoid duplicate writes
- Auth/validation errors should never retry -- they require user action
Related Pages
- Authentication -- Middleware chain producing
UNAUTHORIZEDandFORBIDDEN - tRPC Overview -- Procedure types and Zod validation
- Scoring API -- Procedure-specific error scenarios