kitcn

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

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

On this page