> ## 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.

# PHP SDK - Error Handling

> Exception hierarchy, when each is thrown, and PHP try/catch examples for the PHP SDK.

All SDK exceptions extend `WestyxNexus\Exceptions\NexusException` which itself extends `\RuntimeException`. This means you can catch the base class to handle any SDK error, or catch specific subclasses for fine-grained control.

## Exception hierarchy

| Exception                    | Extends             | HTTP status           | When thrown                                                                                             |
| ---------------------------- | ------------------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
| `NexusException`             | `\RuntimeException` | -                     | Base class; not thrown directly                                                                         |
| `NexusUnauthorizedException` | `NexusException`    | 401                   | Invalid API key, slug/key mismatch, or revoked key                                                      |
| `NexusBillingException`      | `NexusException`    | 402                   | Tenant has overdue invoices (>14 days)                                                                  |
| `NexusNotFoundException`     | `NexusException`    | 404                   | Unknown subdomain or missing endpoint                                                                   |
| `NexusPublicKeyException`    | `NexusException`    | -                     | public key used with `getSecret`, `getSecretFilePath`, or write methods; thrown before any network call |
| `NexusRateLimitedException`  | `NexusException`    | 429                   | Rate limit exceeded; has `$retryAfterSeconds` property (nullable int)                                   |
| `NexusQuarantinedException`  | `NexusException`    | 429 (quarantine body) | Service quarantined; has `$reason` (string) and `$expiresAt` (\DateTimeImmutable) properties            |

## Imports

```php theme={null}
use WestyxNexus\Exceptions\NexusException;
use WestyxNexus\Exceptions\NexusUnauthorizedException;
use WestyxNexus\Exceptions\NexusBillingException;
use WestyxNexus\Exceptions\NexusNotFoundException;
use WestyxNexus\Exceptions\NexusPublicKeyException;
use WestyxNexus\Exceptions\NexusRateLimitedException;
use WestyxNexus\Exceptions\NexusQuarantinedException;
```

## Catching exceptions

### Basic pattern

```php theme={null}
try {
    $client = NexusClient::create(new NexusConfig(
        baseUrl: $_ENV['NEXUS_URL'],
        apiKey:  $_ENV['NEXUS_API_KEY'],
    ));
} catch (NexusUnauthorizedException $e) {
    // Bad API key or slug/key mismatch
    error_log('nexus: unauthorized - ' . $e->getMessage());
    exit(1);
} catch (NexusBillingException $e) {
    // Tenant has overdue invoices
    error_log('nexus: billing overdue - ' . $e->getMessage());
    exit(1);
} catch (NexusException $e) {
    // Any other SDK error (network failure, 404, etc.)
    error_log('nexus: init failed - ' . $e->getMessage());
    exit(1);
}
```

### Handling rate limits

`NexusRateLimitedException` carries an optional `$retryAfterSeconds` property parsed from the `Retry-After` response header:

```php theme={null}
try {
    $client->sync();
} catch (NexusRateLimitedException $e) {
    $wait = $e->retryAfterSeconds;
    if ($wait !== null) {
        sleep($wait);
        $client->sync(); // retry
    } else {
        error_log('nexus: rate limited - ' . $e->getMessage());
    }
}
```

### Handling quarantine

`NexusQuarantinedException` is thrown when the backend returns a `429` with a quarantine body. The service is blocked until the quarantine expires:

```php theme={null}
try {
    $client->sync();
} catch (NexusQuarantinedException $e) {
    error_log(sprintf(
        'nexus: quarantined until %s - reason: %s',
        $e->expiresAt->format(\DateTimeInterface::ATOM),
        $e->reason,
    ));
    // Cache continues to be served; schedule a retry after $e->expiresAt
}
```

### Reading a secret with a public key

`NexusPublicKeyException` fires synchronously before any network call:

```php theme={null}
try {
    $value = $client->getSecret('DB_PASSWORD');
} catch (NexusPublicKeyException $e) {
    // The client was created with a public key
    error_log('nexus: public keys cannot read secrets - use a secret key');
}
```

## 402 Payment Required - `NexusBillingException`

When the backend returns HTTP 402 (the tenant has at least one invoice overdue by more than 14 days), the SDK:

1. Throws `NexusBillingException` from `NexusClient::create` or `sync()`.
2. Halts the background sync loop - read methods continue to serve the last cached snapshot (or the empty cache if billing was overdue on first connect).

The cache stays valid and readable until the process ends. The billing block clears automatically when a subsequent `sync()` succeeds after the invoice is settled.

```php theme={null}
try {
    $client->sync();
} catch (NexusBillingException $e) {
    // Log a warning and continue serving the cached values
    $logger->warning('nexus: billing overdue - config refresh suspended; cache may be stale');
}
```

<Note>
  The cache is **never cleared** on a billing error. The last-known-good snapshot continues to be served, which is usually the safe behaviour for running services. Only the refresh loop is suspended.
</Note>

## What is NOT thrown as an exception

* **Background TTL syncs** - silently retried. Cache served. Errors are logged via the configured PSR-3 logger at `error` level.
* **Missing config or flag** - `getConfig` returns `$default` (or `null`); `getFlag` returns `$default`.
* **`304 Not Modified`** - treated as success; TTL reset.
* **SSE transport errors** inside `connectStream()` - logged and counted toward `$maxErrors`; method returns normally when the limit is reached.

<Info>
  The asymmetry between secrets (return `null`) and configs/flags (return default) is deliberate: a missing secret often means misconfiguration, while a missing flag almost always means "use the safe default and continue".
</Info>

## Failure scenarios

### Wrong API key

```php theme={null}
try {
    $client = NexusClient::create(new NexusConfig(
        baseUrl: $_ENV['NEXUS_URL'],
        apiKey:  'invalid_key',
    ));
} catch (NexusUnauthorizedException $e) {
    die('nexus: bad API key or slug mismatch');
}
```

### Network down at startup

If the Nexus backend is unreachable when `NexusClient::create` runs, Guzzle throws a `\GuzzleHttp\Exception\ConnectException` which is wrapped in a `NexusException`:

```php theme={null}
try {
    $client = NexusClient::create($config);
} catch (NexusException $e) {
    // Log and decide: crash or retry with backoff
    error_log('nexus: ' . $e->getMessage());
    exit(1);
}
```

### Reading a secret that doesn't exist

```php theme={null}
$value = $client->getSecret('does.not.exist');
// $value is null - no exception thrown

if ($value === null) {
    error_log('nexus: secret not found - using default');
    $value = 'default-value';
}
```
