Error Handling
Handle errors from queries and mutations on the client.
In this guide, we'll explore client-side error handling in cRPC. You'll learn to handle server errors from procedures, work with client-side auth errors, and set up global error handling.
Overview
cRPC provides typed error handling on the client:
| Error Source | Description |
|---|---|
| Server errors | CRPCError thrown in procedures, arrives as ConvexError with error.data |
| Client errors | CRPCClientError generated client-side (e.g., unauthorized query skip) |
Let's explore each one.
Server Errors
When a procedure throws CRPCError, it arrives on the client as a ConvexError. Access the message via error.data:
// Server throws:
throw new CRPCError({ code: 'NOT_FOUND', message: 'Post not found' });
// Client receives:
error.data?.message // 'Post not found'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');
},
})
);Or use toast.promise for a cleaner pattern:
toast.promise(mutation.mutateAsync(data), {
loading: 'Creating...',
success: 'Created!',
error: (e) => e.data?.message ?? 'Failed',
});Type-safe Error Access
When using try/catch:
try {
await mutation.mutateAsync(data);
} catch (err) {
const error = err as Error & { data?: { message?: 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 'better-convex/crpc';
if (isCRPCClientError(error)) {
console.log(error.code); // 'UNAUTHORIZED'
console.log(error.functionName); // 'user:getSettings'
}Error Codes
Here are the available error codes:
| Code | Description |
|---|---|
UNAUTHORIZED | Missing authentication |
FORBIDDEN | Not authorized |
NOT_FOUND | Resource not found |
BAD_REQUEST | Invalid input |
TOO_MANY_REQUESTS | Rate limited |
Check Specific Codes
Check for specific error codes:
import { isCRPCErrorCode } from 'better-convex/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 'better-convex/crpc';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
if (isCRPCClientError(error)) {
console.log(`[CRPC] ${error.code}:`, error.functionName);
}
},
}),
});Tip: Use global error handling for logging and analytics. Handle specific errors in component callbacks for user-facing messages.
Migrate from Convex
If you're coming from vanilla Convex, here's what changes.
What stays the same
- Errors propagate from server to client
- ConvexError structure
What's new
Before (vanilla Convex):
try {
await mutation(args);
} catch (error) {
if (error instanceof ConvexError) {
console.log(error.data); // unknown shape
}
}After (cRPC):
mutation.mutate(args, {
onError: (error) => {
toast.error(error.data?.message ?? 'Failed');
},
});Key differences:
| Feature | Description |
|---|---|
| TanStack Query states | isError, error available on queries |
| Typed callbacks | onError in mutationOptions |
| Consistent access | error.data?.message pattern |
| Client errors | CRPCClientError for auth failures |
| Global handling | Via QueryCache in QueryClient |