How Open-SEO's Error Handling System Works with Custom Error Codes
Open-SEO centralizes error handling around a type-safe catalog of custom error codes, using an AppError class to carry codes from server to client while filtering sensitive details for telemetry.
The every-app/open-seo repository implements a robust error handling system that replaces fragile string-based messages with typed custom error codes. This architecture ensures consistent error propagation across server functions, safe client communication, and controlled telemetry reporting. By leveraging Zod for schema validation and TypeScript for compile-time safety, the system prevents arbitrary error strings from leaking into production monitoring or frontend interfaces.
Defining the Centralized Error Code Catalog
All custom error codes live in src/shared/error-codes.ts as a closed constant array. The team uses Zod to generate a strict enum schema, ensuring every error thrown is a known, validated value rather than an arbitrary string.
const ERROR_CODES = [
"UNAUTHENTICATED", "AUTH_CONFIG_MISSING", "PAYMENT_REQUIRED",
"INSUFFICIENT_CREDITS", "FORBIDDEN", "NOT_FOUND", "AUDIT_CAPACITY_REACHED",
"AUDIT_PAGE_LIMIT_EXCEEDED", "AUDIT_ALREADY_RUNNING", "VALIDATION_ERROR",
"CRAWL_TARGET_BLOCKED", "BACKLINKS_BILLING_ISSUE", "AI_SEARCH_BILLING_ISSUE",
"DATAFORSEO_AUTH_FAILED", "RATE_LIMITED", "UPSTREAM_UNAVAILABLE",
"CONFLICT", "INTERNAL_ERROR",
] as const;
export const errorCodeSchema = z.enum(ERROR_CODES);
export type ErrorCode = z.infer<typeof errorCodeSchema>;
Controlling Telemetry with Non-Reportable Codes
Not every error warrants monitoring. The system maintains a NON_REPORTABLE_ERROR_CODES set that includes expected failures like authentication gaps or validation issues. The shouldCaptureAppErrorCode function checks this set before sending events to PostHog or other observability platforms.
const NON_REPORTABLE_ERROR_CODES = new Set<ErrorCode>([
"UNAUTHENTICATED", "NOT_FOUND", "PAYMENT_REQUIRED", "INSUFFICIENT_CREDITS",
"VALIDATION_ERROR", "AUDIT_CAPACITY_REACHED", "AUDIT_PAGE_LIMIT_EXCEEDED",
"AUDIT_ALREADY_RUNNING",
]);
export function shouldCaptureAppErrorCode(
code: ErrorCode | null | undefined,
): boolean {
return code == null || !NON_REPORTABLE_ERROR_CODES.has(code);
}
The AppError Class and Runtime Normalization
Located in src/server/lib/errors.ts, the AppError class extends the native Error object to carry a mandatory code property and optional structured details.
export class AppError extends Error {
constructor(
public readonly code: ErrorCode,
message?: string,
public readonly details?: Record<string, string>,
) {
super(message ?? code);
this.name = "AppError";
}
}
Normalizing Arbitrary Errors
The asAppError utility converts unknown thrown values into AppError instances when possible. This ensures downstream handlers always work with typed error objects rather than untrusted message strings.
export function asAppError(error: unknown): AppError | null {
if (error instanceof AppError) return error;
if (error instanceof Error && isErrorCode(error.message)) {
return new AppError(error.message, error.message);
}
return null;
}
Safe Error Transmission to the Client
The toClientError function in src/server/lib/errors.ts acts as a security gate. It maps server-side AppError instances to generic Error objects suitable for public exposure, preventing internal implementation details from reaching the browser.
const CLIENT_DETAIL_ERROR_CODES = new Set<ErrorCode>(["AUTH_CONFIG_MISSING"]);
export function toClientError(error: unknown): Error {
const appError = asAppError(error);
if (
appError &&
CLIENT_DETAIL_ERROR_CODES.has(appError.code) &&
appError.message !== appError.code
) {
return new Error(`${appError.code}: ${appError.message}`);
}
return new Error(appError?.code ?? "INTERNAL_ERROR");
}
Only codes listed in CLIENT_DETAIL_ERROR_CODES retain their original message; all others return just the error code or a safe fallback, effectively sanitizing the response.
Implementing Custom Error Codes in Production
Throwing Typed Errors in Server Functions
Server functions import AppError and throw with specific codes to signal exact failure modes. For example, in src/serverFunctions/ai-search.ts, the system validates plan status before processing:
if (!await customerHasPaidPlan(organizationId)) {
throw new AppError(
"PAYMENT_REQUIRED",
"Upgrade to the paid plan to use AI Visibility",
);
}
Integrating with Telemetry Pipelines
Before reporting to monitoring tools, calling code checks shouldCaptureAppErrorCode to exclude expected user-facing errors like VALIDATION_ERROR or INSUFFICIENT_CREDITS from analytics dashboards. This keeps signal-to-noise ratio high by only capturing genuine system anomalies.
Summary
- Centralized catalog: All custom error codes are defined in
src/shared/error-codes.tswith Zod enforcing compile-time type safety across the entire application. - Typed transport: The
AppErrorclass carries error codes and server-side metadata from business logic through to error boundaries. - Privacy controls:
toClientErrorstrips internal details before sending responses to the browser, whileshouldCaptureAppErrorCodeprevents expected user errors from polluting telemetry. - Extensibility: Adding new codes requires only appending to the
ERROR_CODESarray; the Zod schema and TypeScript types regenerate automatically.
Frequently Asked Questions
How do I add a new custom error code to Open-SEO?
Append your identifier to the ERROR_CODES array in src/shared/error-codes.ts. The errorCodeSchema Zod enum and the ErrorCode TypeScript type regenerate automatically, making the new code available for use in AppError constructors throughout the server codebase without additional configuration.
Why are some error codes excluded from telemetry reporting?
The NON_REPORTABLE_ERROR_CODES set includes expected user-triggered failures like UNAUTHENTICATED or VALIDATION_ERROR. The shouldCaptureAppErrorCode function filters these out before sending events to PostHog, ensuring monitoring dashboards highlight genuine infrastructure issues rather than routine user mistakes.
How does the system prevent sensitive error details from leaking to clients?
The toClientError function in src/server/lib/errors.ts maps internal AppError instances to safe client errors. Unless an error code appears in the CLIENT_DETAIL_ERROR_CODES whitelist, the function strips the original message and returns only the error code string (or INTERNAL_ERROR), protecting internal stack traces and database constraint violations from external exposure.
Can I attach additional context to an error without exposing it to the client?
Yes. The AppError constructor accepts a details parameter of type Record<string, string>. This metadata remains server-side and is never passed through toClientError, making it ideal for logging request IDs or internal diagnostic data while keeping client responses minimal and secure.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →