# OpenSEO Error Handling Patterns: A Complete Guide to Type-Safe Failure Management

> Master OpenSEO error handling with type-safe failure management. Explore AppError, middleware, and telemetry for robust applications. Learn the complete guide to OpenSEO error patterns.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: deep-dive
- Published: 2026-09-06

---

**OpenSEO uses a layered, type-safe error-handling architecture built around the `AppError` class, centralized middleware, and selective telemetry to PostHog.**

The OpenSEO repository implements a disciplined approach to error handling in TypeScript that spans both server and client environments. Understanding these patterns is essential for contributing to the codebase or adapting similar strategies in your own projects.

## Core Architecture: Four Layers of Error Handling

OpenSEO organizes error handling into distinct layers, each with a specific responsibility. This separation ensures business logic remains clean while failures are consistently tracked and safely exposed.

### Domain Errors with `AppError`

At the foundation lies the `AppError` class defined in [`src/server/lib/errors.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/errors.ts). This class represents business-logic failures using **stable error codes** rather than fragile string messages.

```typescript
import { AppError } from '@/server/lib/errors';

throw new AppError('PROJECT_NOT_FOUND', `Project ${id} does not exist`);

```

The constructor signature requires:
- A code from the canonical `ERROR_CODES` list
- An optional human-readable message
- Optional additional context

### Validation Error Conversion

TanStack server-function validation failures are normalized through the `isValidatorError` helper in [`src/middleware/errorHandling.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/errorHandling.ts). This helper detects validator errors and transforms them into `AppError` instances with the `VALIDATION_ERROR` code.

### Error Normalization Utilities

The same middleware file provides two critical conversion functions:

- **`asAppError(error)`** — Attempts to coerce any thrown value to an `AppError`, returning `null` if impossible
- **`toClientError(appError)`** — Strips internal details to produce a safe, serializable error object

```typescript
import { asAppError, toClientError } from '@/server/lib/errors';

try {
  await riskyOperation();
} catch (e) {
  const appError = asAppError(e) ?? new AppError('INTERNAL_ERROR');
  throw toClientError(appError); // Safe for client consumption
}

```

## Error Handling Middleware: The Central Guard

The `errorHandlingMiddleware` in [`src/middleware/errorHandling.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/errorHandling.ts) wraps every server function to apply uniform error processing. It integrates with TanStack's middleware system via `createMiddleware`.

### Execution Flow

1. **Wraps** the downstream handler in a try/catch block
2. **Classifies** caught errors (validator vs. unknown vs. `AppError`)
3. **Coerces** non-`AppError` values using `asAppError`
4. **Filters** telemetry via `shouldCaptureAppErrorCode`
5. **Reports** eligible errors to PostHog through `captureServerError`
6. **Sanitizes** the response using `toClientError` before re-throwing

### Request Context Capture

When reporting occurs, the middleware gathers:
- HTTP method and path
- Resolved user distinct ID via `resolveErrorDistinctId`
- Authenticated state (without exposing tokens)

```typescript
import { errorHandlingMiddleware } from '@/middleware/errorHandling';
import { createServerFn } from '@tanstack/react-start/server';

export const secureServerFn = createServerFn()
  .use(errorHandlingMiddleware) // Installs full error stack
  .handler(async (params) => {
    // Your logic here; all errors handled uniformly
  });

```

Global registration happens in [`src/serverFunctions/middleware.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/middleware.ts), ensuring every server function inherits this protection.

## Telemetry Control and Observability

OpenSEO implements **selective error reporting** to avoid analytics noise. The [`src/shared/error-codes.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/error-codes.ts) file defines both the canonical error code list and the filtering logic.

### Opting In or Out of Reporting

```typescript
// src/shared/error-codes.ts
export const ERROR_CODES = [
  'PROJECT_NOT_FOUND',
  'VALIDATION_ERROR',
  'AUTH_CONFIG_MISSING',
  'CUSTOM_FEATURE_LIMIT_EXCEEDED',
  // ... more codes
] as const;

export function shouldCaptureAppErrorCode(code?: string): boolean {
  const BLOCKED = new Set(['AUTH_CONFIG_MISSING', 'RATE_LIMIT_SILENT']);
  return !BLOCKED.has(code ?? '');
}

```

Only errors passing this filter reach `captureServerError` in [`src/server/lib/posthog.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/posthog.ts), which asynchronously sends structured data to PostHog inside `waitUntil` to avoid blocking responses.

## Security: Client-Safe Error Exposure

The `toClientError` function enforces a critical boundary: **internal details never leak to the client**. The transformation:

- Preserves only the error code and safe message
- Removes stack traces
- Strips any internal context objects
- Falls back to a generic message for truly unexpected errors

This pattern satisfies security requirements while maintaining debugging capability through server-side logs and telemetry.

## Key Files Reference

| File | Responsibility |
|------|---------------|
| [`src/server/lib/errors.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/errors.ts) | `AppError` class, `asAppError`, `toClientError` |
| [`src/middleware/errorHandling.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/errorHandling.ts) | Central middleware, validator detection, request context |
| [`src/shared/error-codes.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/error-codes.ts) | Canonical codes, `shouldCaptureAppErrorCode` |
| [`src/server/lib/posthog.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/posthog.ts) | `captureServerError`, PostHog integration |
| [`src/serverFunctions/middleware.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/middleware.ts) | Global middleware registration |

## Summary

- **Type safety first**: All errors flow through `AppError` with compile-time code validation
- **Middleware encapsulation**: `errorHandlingMiddleware` provides consistent behavior without boilerplate
- **Selective telemetry**: `shouldCaptureAppErrorCode` prevents analytics pollution
- **Security by default**: `toClientError` guarantees safe client exposure
- **Observability**: PostHog integration with request context enables effective debugging

## Frequently Asked Questions

### How does OpenSEO distinguish between validation and domain errors?

The `isValidatorError` helper in [`src/middleware/errorHandling.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/errorHandling.ts) detects TanStack validator errors by checking error structure, then wraps them as `AppError` instances with code `VALIDATION_ERROR`. Domain errors are thrown directly as `AppError` instances from business logic.

### What happens when an unexpected non-Error value is thrown?

The `asAppError` function attempts to extract meaningful information. If coercion fails, the middleware creates a generic `AppError('INTERNAL_ERROR')`. This ensures type safety even when third-party code throws primitives or unexpected objects.

### Can I disable error reporting for specific error codes?

Yes. Add the code to the `BLOCKED` Set in [`src/shared/error-codes.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/error-codes.ts) within `shouldCaptureAppErrorCode`. Errors with blocked codes proceed through normal handling but skip the `captureServerError` telemetry step.

### How does authentication affect error tracking?

The `resolveErrorDistinctId` function in the error handling middleware extracts the authenticated user's analytics ID when available. This attaches user context to PostHog events without exposing identifiers in client-facing responses.