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); // 404isCRPCError
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:
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:
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:
| Code | Description | HTTP |
|---|---|---|
BAD_REQUEST | Invalid request or input validation failed | 400 |
UNAUTHORIZED | Missing or invalid authentication | 401 |
PAYMENT_REQUIRED | Payment required to access resource | 402 |
FORBIDDEN | Authenticated but not authorized | 403 |
NOT_FOUND | Resource not found | 404 |
METHOD_NOT_SUPPORTED | Method not supported | 405 |
TIMEOUT | Request timeout | 408 |
CONFLICT | Resource conflict | 409 |
PRECONDITION_FAILED | Precondition failed | 412 |
PAYLOAD_TOO_LARGE | Request too large | 413 |
UNPROCESSABLE_CONTENT | Valid syntax but cannot process | 422 |
TOO_MANY_REQUESTS | Rate limit exceeded | 429 |
INTERNAL_SERVER_ERROR | Unexpected server error | 500 |