# How Open-SEO's Error Handling System Works with Custom Error Codes

> Learn how Open-SEO's error handling uses custom error codes and the AppError class to manage server-to-client error communication securely and efficiently.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-07-30

---

**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`](https://github.com/every-app/open-seo/blob/main/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.

```ts
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.

```ts
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`](https://github.com/every-app/open-seo/blob/main/src/server/lib/errors.ts), the `AppError` class extends the native `Error` object to carry a mandatory `code` property and optional structured details.

```ts
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.

```ts
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`](https://github.com/every-app/open-seo/blob/main/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.

```ts
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`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/ai-search.ts), the system validates plan status before processing:

```ts
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.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/error-codes.ts) with Zod enforcing compile-time type safety across the entire application.
- **Typed transport**: The `AppError` class carries error codes and server-side metadata from business logic through to error boundaries.
- **Privacy controls**: `toClientError` strips internal details before sending responses to the browser, while `shouldCaptureAppErrorCode` prevents expected user errors from polluting telemetry.
- **Extensibility**: Adding new codes requires only appending to the `ERROR_CODES` array; 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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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.