> ## Documentation Index
> Fetch the complete documentation index at: https://docs.westyx.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# React SDK - Error Handling

> Error hierarchy, Error Boundary patterns, and 5xx backoff in the React SDK.

The SDK distinguishes between two classes of failures:

1. **Initial sync failures** - surfaced via `NexusInitError` from `NexusProvider`. Handle with an Error Boundary or the `onError` render prop.
2. **Background sync / SSE failures** - never thrown. They are logged via `NexusLogger.error(...)`, and the existing cache continues to be served.

## Error hierarchy

All SDK errors extend `NexusError`. A single `instanceof` check covers everything:

```tsx theme={null}
import { NexusError } from '@westyx-nexus/sdk-react';

if (err instanceof NexusError) {
  // SDK-specific failure - handle as needed
}
```

The full hierarchy:

```
Error
└── NexusError                          (base class)
    ├── NexusInitError                  (initial sync failure - cause has detail)
    ├── NexusUnauthorizedError          (HTTP 401)
    ├── NexusBillingError               (HTTP 402 - tenant has overdue invoices)
    ├── NexusAbAddonNotAvailableError   (HTTP 403 from evaluateAB - add-on disabled)
    ├── NexusServerError                (HTTP 5xx on /v1/sync, caught internally by the retry loop, v0.3.1)
    ├── NexusQuarantinedError           (HTTP 429 quarantine body - sync paused until expiresAt, v0.4.0)
    ├── NexusNotFoundError              (HTTP 404)
    ├── NexusPublicKeyError             (imperative getSecret with public key)
    ├── NexusServiceKindMismatchError   (imperative getSecret on kind=frontend service)
    └── NexusSecretNotFoundError        (imperative getSecret with unknown key)
```

## When each is thrown

| Error                                      | Thrown by                                           | When                                                                                                                                                                                                                                                      |
| ------------------------------------------ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NexusInitError`                           | `NexusProvider` (during mount)                      | The initial sync failed for any reason. The `cause` property carries the underlying error.                                                                                                                                                                |
| `NexusUnauthorizedError`                   | Initial sync, manual `nexus.sync()`                 | Server returned HTTP 401 - invalid API key, slug/key mismatch, or revoked key.                                                                                                                                                                            |
| `NexusBillingError` *(v0.2.0)*             | Initial sync, background sync, `evaluateAB`         | Server returned HTTP 402 - tenant has invoices overdue by more than 14 days. Background sync halts; the cache continues to serve. Use `useNexusBillingOverdue()` to detect the state.                                                                     |
| `NexusAbAddonNotAvailableError` *(v0.3.0)* | `useEvaluateAB()` callback, `nexus.evaluateAB(...)` | Server returned HTTP 403 - the AB Testing add-on is not active for the tenant. Only `evaluate-ab` is gated; `useFlag` / `getFlag` keep working.                                                                                                           |
| `NexusServerError` *(v0.3.1)*              | `nexus.sync()` (caught internally)                  | Server returned HTTP 5xx on `/v1/sync`. The SDK catches this and schedules an exponential-backoff retry - consumers normally don't see it. Exported for `instanceof` checks in custom hooks.                                                              |
| `NexusQuarantinedError` *(v0.4.0)*         | `nexus.sync()`                                      | Server returned HTTP 429 with `{"error":"quarantined"}` body. The `reason` (string) and `expiresAt` (Date) properties expose detail. The SDK automatically pauses background sync until `expiresAt`. The `onQuarantined` observer fires at the same time. |
| `NexusNotFoundError`                       | Initial sync, manual `nexus.sync()`, `evaluateAB`   | Server returned HTTP 404 - unknown subdomain or missing endpoint.                                                                                                                                                                                         |
| `NexusPublicKeyError`                      | `useNexus().getSecret(...)`                         | The client uses a public key. Thrown synchronously by the imperative method. The hook `useSecret()` returns `undefined` instead.                                                                                                                          |
| `NexusServiceKindMismatchError` *(v0.2.0)* | `useNexus().getSecret(...)`                         | The service `kind` reported by the server is `"frontend"`. The hook `useSecret()` returns `undefined` instead.                                                                                                                                            |
| `NexusSecretNotFoundError`                 | `useNexus().getSecret(...)`                         | The requested key is not in the cache. The hook `useSecret()` returns `undefined` instead.                                                                                                                                                                |

## Hooks vs imperative - non-throwing vs throwing

A subtle but deliberate API decision:

```tsx theme={null}
// Hooks - never throw, return undefined for missing/inaccessible
const stripe = useSecret('stripe.key');         // string | undefined
const banner = useConfig<string>('banner');     // string | undefined
```

```tsx theme={null}
// Imperative - throw on missing or inaccessible
const nexus = useNexus();
const stripe = nexus.getSecret('stripe.key');   // throws if missing or public key
```

Why? React component code reads from the cache on every render. If `useSecret` threw on a public key, every render would throw, breaking the component tree. The hook trades type safety (no `string` guarantee) for ergonomic component code.

## Handling `NexusInitError`

### Error Boundary (idiomatic React)

```tsx theme={null}
import { ErrorBoundary } from 'react-error-boundary';
import { NexusInitError, NexusProvider } from '@westyx-nexus/sdk-react';

function NexusErrorFallback({ error }: { error: Error }) {
  if (error instanceof NexusInitError) {
    return <h1>Configuration unavailable - try again later</h1>;
  }
  throw error;  // not for us - bubble up
}

<ErrorBoundary FallbackComponent={NexusErrorFallback}>
  <NexusProvider config={config}>
    <App />
  </NexusProvider>
</ErrorBoundary>
```

### `onError` render prop

Inline handling without an Error Boundary:

```tsx theme={null}
<NexusProvider
  config={config}
  onError={(err) => {
    if (err instanceof NexusInitError && err.cause instanceof NexusUnauthorizedError) {
      return <h1>Bad credentials - check NEXUS_API_KEY</h1>;
    }
    return <h1>Service temporarily unavailable</h1>;
  }}
>
  <App />
</NexusProvider>
```

## What is NOT thrown

* **Background syncs** - silently retried on the next TTL tick. Cache served.
* **SSE transport errors** - the SDK reconnects with exponential backoff, then falls back to TTL polling after 3 consecutive failures. No exception surfaces.
* **`304 Not Modified`** - treated as success; TTL reset.
* **Hook reads for missing/inaccessible keys** - return `undefined` (or the `defaultValue` for `useFlag`).

## SSE-specific behaviour

SSE failures **never** propagate to your code. Use `useNexusStreamStatus()` to detect "live updates are no longer available":

```tsx theme={null}
const { connected } = useNexusStreamStatus();

if (!connected) {
  return <Banner>Live updates paused - values may lag up to 60 s</Banner>;
}
```

## 5xx exponential backoff (v0.3.1+)

When the server returns a `5xx` status on `/sync`, the SDK:

1. Catches the response internally and surfaces it as a `NexusServerError(status, bodySnippet)`.

2. Increments an internal attempt counter and schedules a retry via `setTimeout` with **exponential backoff**:

   | Attempt | Delay         |
   | ------- | ------------- |
   | 1       | 1 s           |
   | 2       | 2 s           |
   | 3       | 4 s           |
   | 4       | 8 s           |
   | 5       | 16 s          |
   | 6+      | 30 s (capped) |

3. Emits `FALLBACK_REASON_TRANSPORT` on the stream observer during the wait.

4. On the next successful sync - including the auto-retry succeeding - resets the counter.

```tsx theme={null}
import { NexusProvider, FALLBACK_REASON_TRANSPORT } from '@westyx-nexus/sdk-react';

<NexusProvider
  config={{
    baseUrl: '...',
    apiKey:  '...',
    streamObserver: {
      onFallback(reason) {
        if (reason === FALLBACK_REASON_TRANSPORT) {
          // 5xx wait - surface "syncing..." in the UI
        }
      },
    },
  }}
>
  <App />
</NexusProvider>
```
