# Ticket 0.5 — Error Handling Foundation

**Phase**: 0 — Project Scaffolding
**Priority**: High
**Depends on**: 0.1

## Description

Define a foundational error handling strategy: structured error classes, consistent error formatting, and a top-level error boundary. This ensures errors are handled uniformly from Phase 1 onwards, rather than retrofitting error handling across all modules in Phase 6.

This ticket produces the `src/lib/errors.ts` module and establishes patterns that all subsequent tickets follow.

## Acceptance Criteria

### AC1: Structured error classes

```gherkin
Given src/lib/errors.ts exists
When I inspect its contents
Then it exports a base RelayError class extending Error with:
  - code: string (machine-readable error code, e.g., "OPENCODE_UNREACHABLE")
  - statusCode: number (HTTP status code, e.g., 502)
  - context: Record<string, unknown> (additional structured data)
  - cause?: Error (original error for chaining)
And it exports specific subclasses:
  - OpenCodeConnectionError (server unreachable / connection refused)
  - OpenCodeApiError (4xx/5xx from OpenCode API, includes endpoint and response)
  - SSEConnectionError (SSE stream disconnected)
  - WebSocketError (browser WebSocket issues)
  - AuthenticationError (PIN failures, rate limiting)
  - ConfigurationError (missing config, invalid values)
```

### AC2: Error formatting for different consumers

```gherkin
Given a RelayError is thrown
When it is caught by the HTTP server
Then it responds with JSON: { error: { code, message, details? } }
And the HTTP status code matches the error's statusCode

When it is caught by the WebSocket handler
Then it sends a WebSocket message: { type: "error", code, message }

When it is logged
Then the log includes: code, message, context, and cause stack trace
And sensitive data (PINs, auth tokens) is NOT included in logs
```

### AC3: Top-level error boundary

```gherkin
Given the relay server is running
When an uncaught exception occurs
Then it is caught by the process-level error handler
And it logs the full error with stack trace
And it does NOT crash the server (for recoverable errors)
And it DOES shut down gracefully for fatal errors (port in use, out of memory)
```

### AC4: Error wrapping utility

```gherkin
Given a low-level error (e.g., fetch network error)
When it is caught in the OpenCode bridge layer
Then it is wrapped in the appropriate RelayError subclass
And the original error is preserved as `cause`
And the wrapper adds context (e.g., which endpoint failed, which session)
```

## Testable Components

| Component | Test Type | What to Assert |
|-----------|-----------|----------------|
| `RelayError` | Unit | Has code, statusCode, context, cause; serialises correctly |
| `OpenCodeConnectionError` | Unit | Correct defaults, inherits from RelayError |
| `OpenCodeApiError` | Unit | Captures endpoint, status, response body |
| Error formatting (HTTP) | Unit | Returns correct JSON shape and status code |
| Error formatting (WS) | Unit | Sends correct WebSocket message shape |
| Error wrapping | Unit | Preserves cause chain, adds context |
| Sensitive data filtering | Unit | PINs and auth tokens are redacted in logs |

## API Shape

```ts
// src/lib/errors.ts
export class RelayError extends Error {
  constructor(message: string, options: {
    code: string;
    statusCode?: number;
    context?: Record<string, unknown>;
    cause?: Error;
  });

  toJSON(): { error: { code: string; message: string; details?: unknown } };
  toWebSocket(): { type: "error"; code: string; message: string };
}

export class OpenCodeConnectionError extends RelayError { ... }
export class OpenCodeApiError extends RelayError { ... }
export class SSEConnectionError extends RelayError { ... }
export class WebSocketError extends RelayError { ... }
export class AuthenticationError extends RelayError { ... }
export class ConfigurationError extends RelayError { ... }

export function wrapError(error: unknown, ErrorClass: typeof RelayError, context?: Record<string, unknown>): RelayError;
```

## Notes

- Every module from Phase 1 onwards should use these error classes rather than throwing raw `Error` objects
- The Phase 6 error handling ticket (6.2) focuses on **edge cases and polish** (retry strategies, user-facing error messages, graceful degradation) — not foundational error infrastructure
- Error codes should be unique and searchable (e.g., `OPENCODE_UNREACHABLE`, `SSE_DISCONNECTED`, `PIN_INVALID`)
- Consider using a lightweight structured logger (or console-based structured logging) that formats errors consistently
