kitcn

Error Handling

Throw typed errors with error codes and HTTP status mapping.

Throwing Errors

The basic pattern is simple - import CRPCError and throw it with a code:

import { CRPCError } from 'kitcn/server';

throw new CRPCError({
  code: 'NOT_FOUND',
  message: 'Post not found',
});

Constructor

The CRPCError constructor accepts these parameters:

new CRPCError({
  code: CRPCErrorCode;  // Required - see table below
  message?: string;     // Optional, defaults to code
  cause?: unknown;      // Optional, original error
  data?: Record<string, unknown>; // Optional, must be Convex-serializable
});

With Custom Data

Attach small Convex-serializable metadata when the client needs more than a message:

throw new CRPCError({
  code: 'CONFLICT',
  message: 'This domain already exists.',
  data: {
    normalizedDomain,
    existingSiteId,
  },
});

With Cause

When catching external errors, preserve the original stack trace using cause:

try {
  await externalApi.call();
} catch (error) {
  throw new CRPCError({
    code: 'INTERNAL_SERVER_ERROR',
    message: 'External API failed',
    cause: error,
  });
}

This helps with debugging by keeping the full error chain.

Error Codes

For the full error code list, see Error Codes in API Reference below.

Tip: Use UNAUTHORIZED when the user isn't logged in, and FORBIDDEN when they're logged in but don't have permission.

Helper Functions

cRPC provides several helper functions for working with errors.

getCRPCErrorFromUnknown

Wrap unknown errors in CRPCError. Useful in catch blocks:

import { getCRPCErrorFromUnknown } from 'kitcn/crpc';

try {
  await riskyOperation();
} catch (error) {
  throw getCRPCErrorFromUnknown(error);
}

getHTTPStatusCodeFromError

Get the HTTP status code from an error. Useful for HTTP endpoints:

import { getHTTPStatusCodeFromError } from 'kitcn/crpc';

const httpStatus = getHTTPStatusCodeFromError(error); // 404

isCRPCError

Type guard for checking if an error is a CRPCError:

import { isCRPCError } from 'kitcn/crpc';

if (isCRPCError(error)) {
  console.log(error.code); // 'NOT_FOUND'
}

Common Patterns

Here are common error patterns.

Authorization

Check authentication in middleware and throw UNAUTHORIZED:

convex/lib/crpc.ts
const authMiddleware = c.middleware(async ({ ctx, next }) => {
  const user = await getSessionUser(ctx);
  if (!user) {
    throw new CRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({ ctx: { ...ctx, user } });
});

Not Found

Throw NOT_FOUND when a resource doesn't exist:

convex/functions/posts.ts
export const getById = authQuery
  .input(z.object({ id: z.string() }))
  .query(async ({ ctx, input }) => {
    const post = await ctx.orm.query.posts.findFirst({
      where: { id: input.id },
    });
    if (!post) {
      throw new CRPCError({
        code: 'NOT_FOUND',
        message: 'Post not found',
      });
    }
    return post;
  });

Rate Limiting

Throw TOO_MANY_REQUESTS when rate limits are exceeded:

if (isRatelimited) {
  throw new CRPCError({
    code: 'TOO_MANY_REQUESTS',
    message: 'Rate limit exceeded. Try again later.',
  });
}

Permission Check

Throw FORBIDDEN when the user doesn't have permission:

if (post.authorId !== ctx.userId) {
  throw new CRPCError({
    code: 'FORBIDDEN',
    message: 'You can only edit your own posts',
  });
}

Input Validation

Throw BAD_REQUEST for custom validation failures (beyond Zod validation):

if (input.startDate > input.endDate) {
  throw new CRPCError({
    code: 'BAD_REQUEST',
    message: 'Start date must be before end date',
  });
}

Next Steps

API Reference

Error Codes

All available error codes with their HTTP status mappings:

CodeDescriptionHTTP
BAD_REQUESTInvalid request or input validation failed400
UNAUTHORIZEDMissing or invalid authentication401
PAYMENT_REQUIREDPayment required to access resource402
FORBIDDENAuthenticated but not authorized403
NOT_FOUNDResource not found404
METHOD_NOT_SUPPORTEDMethod not supported405
TIMEOUTRequest timeout408
CONFLICTResource conflict409
PRECONDITION_FAILEDPrecondition failed412
PAYLOAD_TOO_LARGERequest too large413
UNPROCESSABLE_CONTENTValid syntax but cannot process422
TOO_MANY_REQUESTSRate limit exceeded429
INTERNAL_SERVER_ERRORUnexpected server error500

On this page