Error Handling
Handle errors from queries and mutations on the client.
Server Errors
When a procedure throws CRPCError, it arrives on the client as a ConvexError. Access the built-in fields and any custom payload via error.data:
// Server throws:
throw new CRPCError({
code: 'CONFLICT',
message: 'Domain already exists',
data: { existingSiteId: 'site_123' },
});
// Client receives:
error.data?.message // 'Domain already exists'
error.data?.existingSiteId // 'site_123'Query Errors
Handle errors from queries:
const { data, error, isError } = useQuery(crpc.posts.get.queryOptions({ id }));
if (isError) {
const message = error.data?.message ?? 'Something went wrong';
}Mutation Errors
Handle errors with callbacks:
const mutation = useMutation(
crpc.posts.create.mutationOptions({
onError: (error) => {
toast.error(error.data?.message ?? 'Failed to create post');
},
})
);Prefer error.data?.message before error.message. data.message is the
clean app-level message from CRPCError. error.message can include noisier
transport details.
Or use toast.promise for a cleaner pattern:
toast.promise(mutation.mutateAsync(data), {
loading: 'Creating...',
success: 'Created!',
error: (e) => e.data?.message ?? 'Failed',
});If most mutations should toast the same way, move that to your QueryClient
and keep components lean:
import { QueryCache, QueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
if (isCRPCClientError(error)) {
console.log(`[CRPC] ${error.code}:`, error.functionName);
}
},
}),
defaultOptions: {
mutations: {
onError: (err, _variables, _context, mutation) => {
const error = err as Error & { data?: { message?: string } };
const meta = mutation.meta as
| { errorMessage?: string; skipErrorToast?: boolean }
| undefined;
if (meta?.skipErrorToast) return;
toast.error(
error.data?.message ??
meta?.errorMessage ??
error.message ??
'Something went wrong'
);
},
},
},
});Then set a fallback message per mutation without repeating onError:
const sendMessage = useMutation(
crpc.messages.sendMessage.mutationOptions({
meta: { errorMessage: 'Failed to send message' },
onSuccess: () => {
setDraft('');
toast.success('Message sent');
},
})
);Type-safe Error Access
When using try/catch:
try {
await mutation.mutateAsync(data);
} catch (err) {
const error = err as Error & {
data?: {
message?: string;
existingSiteId?: string;
};
};
toast.error(error.data?.message ?? 'Operation failed');
}Client Errors
CRPCClientError is thrown client-side when queries are skipped due to auth requirements:
import { CRPCClientError, isCRPCClientError } from 'kitcn/crpc';
if (isCRPCClientError(error)) {
console.log(error.code); // 'UNAUTHORIZED'
console.log(error.functionName); // 'user:getSettings'
}See API Reference for all available error codes.
Check Specific Codes
Check for specific error codes:
import { isCRPCErrorCode } from 'kitcn/crpc';
if (isCRPCErrorCode(error, 'UNAUTHORIZED')) {
router.push('/login');
}Global Error Handling
Handle errors globally in the QueryClient:
import { QueryCache, QueryClient } from '@tanstack/react-query';
import { isCRPCClientError } from 'kitcn/crpc';
import { toast } from 'sonner';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
if (isCRPCClientError(error)) {
console.log(`[CRPC] ${error.code}:`, error.functionName);
}
},
}),
defaultOptions: {
mutations: {
onError: (err, _variables, _context, mutation) => {
const error = err as Error & { data?: { message?: string } };
const meta = mutation.meta as
| { errorMessage?: string; skipErrorToast?: boolean }
| undefined;
if (meta?.skipErrorToast) return;
toast.error(
error.data?.message ??
meta?.errorMessage ??
error.message ??
'Something went wrong'
);
},
},
},
});Tip: Use global mutation error handling for user-facing fallback toasts. Keep per-mutation onError only for custom flows that genuinely need bespoke behavior.
Next Steps
API Reference
Error Codes
| Code | Description |
|---|---|
UNAUTHORIZED | Missing authentication |
FORBIDDEN | Not authorized |
NOT_FOUND | Resource not found |
BAD_REQUEST | Invalid input |
TOO_MANY_REQUESTS | Rate limited |