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 '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:

CodeDescription
UNAUTHORIZEDMissing authentication
FORBIDDENNot authorized
NOT_FOUNDResource not found
BAD_REQUESTInvalid input
TOO_MANY_REQUESTSRate 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:

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

On this page