Error Handling

haystack-py uses a structured exception hierarchy to distinguish between different failure modes. This guide covers the exception types, error grids, and patterns for robust error handling.

See also

Core for the full exception API reference.

Exception Hierarchy

HaystackError          Base for all haystack-py exceptions
├── AuthError          Authentication failures (SCRAM, token, cert)
├── CallError          Server returned an error grid
└── NetworkError       Connection, timeout, transport failures

All exceptions inherit from HaystackError, so you can catch everything with a single handler or be specific:

from hs_py import Client, HaystackError, CallError, AuthError, NetworkError

async with Client("http://host/api", "user", "pass") as c:
    try:
        data = await c.read("point")
    except AuthError:
        # Bad credentials, expired token, cert rejected
        ...
    except CallError as e:
        # Server returned an error grid
        print(e.grid)   # The error Grid
        ...
    except NetworkError:
        # Connection refused, timeout, DNS failure
        ...
    except HaystackError:
        # Catch-all for any Haystack error
        ...

CallError and Error Grids

When a Haystack server encounters an error processing a request, it returns an error grid — a grid with error metadata in its meta dict. The client detects error grids and raises CallError.

from hs_py.errors import CallError

try:
    data = await c.read("invalid:::filter")
except CallError as e:
    print(f"Error: {e.dis}")           # Human-readable description
    print(f"Trace: {e.trace}")          # Optional server stack trace (or None)
    print(f"Error grid: {e.grid}")      # The full error Grid
    # e.grid.meta typically contains:
    #   "err": Marker
    #   "dis": "error message"
    #   "errTrace": "stack trace..."

Creating Error Grids

On the server side, create error grids with the factory method:

from hs_py import Grid

err = Grid.make_error("something went wrong")
assert err.is_error

# Check if any grid is an error
if grid.is_error:
    print("Got an error grid:", grid.meta.get("dis"))

Raising Errors in Server Handlers

In HaystackOps methods, raise CallError to return an error grid to the client:

from hs_py.errors import CallError
from hs_py import Grid, HaystackOps

class MyOps(HaystackOps):
    async def read(self, grid: Grid) -> Grid:
        if not grid.meta.get("filter"):
            raise CallError("filter is required", Grid.make_error("filter is required"))
        ...

Unhandled exceptions in handlers are caught by the error middleware and converted to error grids automatically — but prefer explicit errors for known failure modes.

AuthError

AuthError is raised for authentication failures:

  • Wrong username or password during SCRAM handshake

  • Expired or invalid bearer token

  • Rejected client certificate in mTLS

from hs_py import Client
from hs_py.errors import AuthError

try:
    async with Client("http://host/api", "wrong", "creds") as c:
        await c.about()
except AuthError as e:
    print(f"Authentication failed: {e}")

NetworkError

NetworkError wraps transport-level failures:

  • Connection refused or reset

  • DNS resolution failure

  • Request timeout

  • TLS handshake failure

from hs_py import Client
from hs_py.errors import NetworkError

try:
    async with Client("http://unreachable:9999/api") as c:
        await c.about()
except NetworkError as e:
    print(f"Connection failed: {e}")

Common Patterns

Retry with Backoff

import asyncio
from hs_py import Client, NetworkError

async def read_with_retry(c: Client, filter_str: str, max_retries: int = 3):
    for attempt in range(max_retries):
        try:
            return await c.read(filter_str)
        except NetworkError:
            if attempt == max_retries - 1:
                raise
            await asyncio.sleep(2 ** attempt)

Graceful Degradation

from hs_py import Client, CallError, NetworkError

async def safe_read(c: Client, filter_str: str):
    try:
        return await c.read(filter_str)
    except CallError:
        # Server can't fulfill the request — return empty
        return Grid.make_empty()
    except NetworkError:
        # Server unreachable — return cached data or empty
        return Grid.make_empty()

Distinguishing Error Types

from hs_py import HaystackError, CallError, AuthError, NetworkError

try:
    result = await c.read("point")
except AuthError:
    # Credentials issue — re-authenticate or alert admin
    log.error("Authentication expired, re-authenticating")
except CallError as e:
    # Application error — log and investigate
    log.warning("Server error: %s", e.grid.meta.get("dis"))
except NetworkError:
    # Infrastructure issue — retry or failover
    log.error("Network unreachable, switching to backup")
except HaystackError:
    # Unexpected — log full context
    log.exception("Unexpected Haystack error")