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

# Error Handling and the NSP SDK Exception Hierarchy

> Every exception nelieo-nsp raises: when it triggers, which fields it exposes, and recovery patterns including exponential backoff for NSPRateLimitError.

Every error raised by the `nelieo-nsp` SDK is a subclass of `NSPError`. Catching the right exception at the right level lets you distinguish between transient network failures, authentication problems, missing apps, and application-level action failures — and handle each one appropriately.

## Exception Hierarchy

```python theme={null}
NSPError                              # Base class for all SDK errors
├── NSPConnectionError                # Daemon unreachable
├── NSPAuthError                      # Invalid or missing API key
├── NSPNotFoundError                  # App or PID not tracked
├── NSPRateLimitError                 # Too many requests
├── NSPTimeoutError                   # Request or probe timed out
├── NSPConfidenceTooLowError          # Probe confidence below action threshold
├── NSPActionError                    # Action dispatched but failed at app level
│   └── NSPIrreversibleNotConfirmedError  # verify=False on irreversible action
└── NSPServerError                    # Unexpected daemon error (5xx)
```

## Base Error Fields

Every `NSPError` subclass exposes these three attributes:

```python theme={null}
e.message      # str  — human-readable description
e.code         # str  — machine-readable error code
e.status_code  # int  — HTTP status (0 for connection-level errors)
```

## Exception Reference

<AccordionGroup>
  <Accordion title="NSPConnectionError — Daemon unreachable">
    **When raised:** The daemon is not running, is not listening on the configured host and port, or an existing connection was dropped during a request.

    **HTTP status:** 0 (no HTTP response received)

    **Fields:**

    * `e.message` — description of the connection failure
    * `e.code` — `"connection_refused"` or `"connection_reset"`
    * `e.status_code` — `0`

    **How to handle:**

    ```python theme={null}
    try:
        apps = await client.list_apps()
    except nsp.NSPConnectionError as e:
        print(f"Daemon unreachable: {e.message}")
        print("Start the daemon with: axon-daemon.exe run")
        raise
    ```

    Common causes:

    * Daemon not started
    * `NSP_PORT` does not match `AXON_SERVER__PORT` in your daemon config
    * Firewall rule blocking localhost connections
  </Accordion>

  <Accordion title="NSPAuthError — Invalid or missing API key">
    **When raised:** The API key is absent, malformed, expired, or has been disabled on the daemon.

    **HTTP status:** 401

    **Fields:**

    * `e.message` — human-readable auth failure description
    * `e.code` — one of `"missing_key"`, `"invalid_key"`, `"disabled_key"`
    * `e.status_code` — `401`

    **How to handle:**

    ```python theme={null}
    try:
        apps = await client.list_apps()
    except nsp.NSPAuthError as e:
        print(f"Auth failed [{e.code}]: {e.message}")
        print("Generate a new key: axon-daemon.exe keygen")
        raise
    ```

    Generate a fresh key and set it as an environment variable or pass it to `NSPClient(api_key=...)`.
  </Accordion>

  <Accordion title="NSPNotFoundError — App or PID not tracked">
    **When raised:** The app name, `app_id`, or PID you requested does not exist in the daemon's tracked list.

    **HTTP status:** 404

    **Fields:**

    * `e.message` — which resource was not found
    * `e.code` — `"app_not_found"` or `"pid_not_found"`
    * `e.status_code` — `404`

    **How to handle:**

    ```python theme={null}
    try:
        session = await client.attach("Gmail")
    except nsp.NSPNotFoundError:
        apps = await client.list_apps()
        available = [a.app_name for a in apps]
        print(f"App not found. Available: {available}")

        # If the app might still be launching, wait for it
        session = await client.wait_for_app("Gmail", timeout=60.0)
    ```
  </Accordion>

  <Accordion title="NSPRateLimitError — Too many requests">
    **When raised:** You have exceeded the daemon's per-key rate limit for state reads or action dispatches.

    **HTTP status:** 429

    **Fields:**

    * `e.message` — rate limit description
    * `e.code` — `"rate_limit_exceeded"`
    * `e.status_code` — `429`
    * `e.retry_after` — `float` — minimum seconds to wait before retrying

    **How to handle with exponential backoff:**

    ```python theme={null}
    import asyncio

    async def with_backoff(coro_fn, max_retries: int = 5):
        """Call coro_fn(), retrying on rate-limit with exponential backoff."""
        for attempt in range(max_retries):
            try:
                return await coro_fn()
            except nsp.NSPRateLimitError as e:
                if attempt == max_retries - 1:
                    raise
                # Honor the daemon's retry_after if provided
                wait = max(e.retry_after or 0, 2 ** attempt)
                print(f"Rate limited. Retrying in {wait:.1f}s "
                      f"(attempt {attempt + 1}/{max_retries})")
                await asyncio.sleep(wait)
    ```

    Default limits: 100 state reads/sec and 10 actions/sec per API key. Adjust in `axon.toml`.
  </Accordion>

  <Accordion title="NSPTimeoutError — Request or probe timed out">
    **When raised:** A request exceeded `NSPClient`'s `timeout` setting, or a fresh probe cycle did not complete within the daemon's probe timeout.

    **HTTP status:** 504

    **Fields:**

    * `e.message` — what timed out
    * `e.code` — `"request_timeout"` or `"probe_timeout"`
    * `e.status_code` — `504`

    **How to handle:**

    ```python theme={null}
    try:
        # Fresh probes can be slow for CLR/JVM targets
        state = await client.get_state(pid, fresh=True)
    except nsp.NSPTimeoutError:
        # Fall back to cached state
        state = await client.get_state(pid, fresh=False)
        print(f"Using cached state — confidence={state.confidence:.2f}")
    ```
  </Accordion>

  <Accordion title="NSPConfidenceTooLowError — Probe not ready">
    **When raised:** The probe's confidence score is below the minimum required to execute the requested action. This prevents the SDK from acting on stale or incomplete state.

    **HTTP status:** 422

    **Fields:**

    * `e.message` — description including current and required confidence
    * `e.code` — `"confidence_too_low"`
    * `e.status_code` — `422`
    * `e.current_confidence` — `float` — the probe's current score
    * `e.required_confidence` — `float` — minimum score needed

    **When this happens:**

    * The app was just detected and the first probe is not yet complete
    * The app navigated to a new view and confidence dropped temporarily
    * A native app with inherently variable confidence

    **How to handle:**

    ```python theme={null}
    try:
        await session.execute("send_reply", parameters={...}, verify=True)
    except nsp.NSPConfidenceTooLowError as e:
        print(
            f"Confidence {e.current_confidence:.2f} below "
            f"required {e.required_confidence:.2f}"
        )
        # Wait for probe to warm up, then retry
        await asyncio.sleep(5.0)
        await session.refresh(fresh_probe=True)
        # Retry the action once
        await session.execute("send_reply", parameters={...}, verify=True)
    ```
  </Accordion>

  <Accordion title="NSPActionError — Action failed at the application level">
    **When raised:** The action was dispatched to the runtime but failed — either because the application rejected it, the action's internal logic errored, or post-action verification did not pass.

    **Fields:**

    * `e.message` — human-readable failure description
    * `e.code` — machine-readable error code (e.g. `"clr_invoke_timeout"`, `"verification_failed"`)
    * `e.action` — `str` — name of the action that failed
    * `e.verification_failed` — `bool` — `True` if the action ran but `verify_expression` returned falsy

    **How to handle:**

    ```python theme={null}
    try:
        result = await session.execute(
            "send_reply",
            parameters={"thread_id": "msg_18821af3", "body": "On my way."},
            verify=True,
            verify_expression="inbox.sent_count > 0",
        )
    except nsp.NSPActionError as e:
        print(f"Action '{e.action}' failed: {e.message}  code={e.code}")
        if e.verification_failed:
            print("  The action ran but post-action state did not match "
                  "the verify_expression.")
    ```
  </Accordion>

  <Accordion title="NSPIrreversibleNotConfirmedError — Missing verify=True">
    **When raised:** You called `session.execute()` on an `IrreversibleWrite` action without explicitly setting `verify=True`. The SDK enforces this check locally before sending any request to the daemon — no network round-trip is wasted.

    **Fields:**

    * `e.message` — explanation of why the call was rejected
    * `e.code` — `"irreversible_not_confirmed"`
    * `e.action` — `str` — name of the irreversible action

    **How to handle:**

    ```python theme={null}
    # This raises NSPIrreversibleNotConfirmedError:
    await session.execute("delete_thread", parameters={"thread_id": "msg_abc"})

    # Correct — pass verify=True explicitly:
    await session.execute(
        "delete_thread",
        parameters={"thread_id": "msg_abc"},
        verify=True,
        verify_expression="inbox.total_count < prev_total",
    )
    ```
  </Accordion>
</AccordionGroup>

## Comprehensive Try/Except Pattern

Use this template as the foundation for any production agent:

```python theme={null}
import asyncio
import nelieo_nsp as nsp

async def safe_execute(session: nsp.NSPSession, action: str, parameters: dict):
    """
    Execute an action with full error handling for every SDK exception class.
    Returns ActionResponse on success, or re-raises on unrecoverable errors.
    """
    for attempt in range(3):
        try:
            result = await session.execute(
                action,
                parameters=parameters,
                verify=True,
            )
            return result

        except nsp.NSPConnectionError:
            # Daemon went away — nothing we can do here
            print("❌ Daemon connection lost. Is axon-daemon running?")
            raise

        except nsp.NSPAuthError:
            # Key is invalid — not retriable without operator intervention
            print("❌ Invalid API key. Run: axon-daemon.exe keygen")
            raise

        except nsp.NSPNotFoundError:
            # App disappeared (process exited) — not retriable
            print(f"❌ App no longer tracked by daemon")
            raise

        except nsp.NSPConfidenceTooLowError as e:
            print(
                f"⚠ Confidence {e.current_confidence:.2f} < "
                f"required {e.required_confidence:.2f} — waiting 5s"
            )
            await asyncio.sleep(5.0)
            await session.refresh(fresh_probe=True)
            # loop continues to retry

        except nsp.NSPRateLimitError as e:
            wait = max(e.retry_after or 0, 2 ** attempt)
            print(f"⚠ Rate limited. Retrying in {wait:.1f}s...")
            await asyncio.sleep(wait)
            # loop continues to retry

        except nsp.NSPTimeoutError:
            print(f"⚠ Timeout on attempt {attempt + 1}. Retrying...")
            await asyncio.sleep(2.0)
            # loop continues to retry

        except nsp.NSPIrreversibleNotConfirmedError as e:
            # Programming error — must be fixed in code, not retried
            print(f"❌ Action '{e.action}' is irreversible. Set verify=True.")
            raise

        except nsp.NSPActionError as e:
            print(f"❌ Action '{e.action}' failed: {e.message}  code={e.code}")
            if e.verification_failed:
                print("  Action ran but post-action state did not match "
                      "verify_expression.")
            raise

    raise nsp.NSPError("Exceeded maximum retry attempts")


# Usage
async def main():
    async with nsp.NSPClient(api_key="sk_nsp_...") as client:
        session = await client.attach("Gmail")

        result = await safe_execute(
            session,
            "archive",
            {"email_id": session.get_str("inbox.emails[0].id")},
        )
        print(f"Archived: {result.success}  latency={result.latency_ms}ms")

asyncio.run(main())
```

<Warning>
  Never silently swallow `NSPConnectionError` or `NSPAuthError`. These indicate conditions that require operator action and cannot be resolved by retrying.
</Warning>
