BETTER-CONVEX

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 SourceDescription
Server errorsCRPCError thrown in procedures, arrives as ConvexError with error.data
Client errorsCRPCClientError 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 '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';

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:

FeatureDescription
TanStack Query statesisError, error available on queries
Typed callbacksonError in mutationOptions
Consistent accesserror.data?.message pattern
Client errorsCRPCClientError for auth failures
Global handlingVia QueryCache in QueryClient

Next Steps

API Reference

Error Codes

CodeDescription
UNAUTHORIZEDMissing authentication
FORBIDDENNot authorized
NOT_FOUNDResource not found
BAD_REQUESTInvalid input
TOO_MANY_REQUESTSRate limited

On this page