# Error Handling

The Cubit SDK provides structured exceptions for all error cases, making it easy to handle failures gracefully.

## Exception Hierarchy

```
CubitError (base)
|-- AuthenticationError (401)
|-- AuthorizationError (403)
|-- NotFoundError (404)
|-- ValidationError (422)
|-- RateLimitError (429)
|-- ServerError (5xx)
```

All exceptions inherit from `CubitError`.

## Basic Error Handling

```python
from cubit import (
    CubitClient,
    CubitError,
    AuthenticationError,
    AuthorizationError,
    NotFoundError,
    RateLimitError,
    ValidationError,
    ServerError,
)

client = CubitClient("cubit_your_key")

try:
    job = client.get_job("99-9999.00")
except NotFoundError as e:
    print(f"Job not found: {e}")
except AuthenticationError as e:
    print(f"Invalid API key: {e}")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after} seconds")
except CubitError as e:
    print(f"API error: {e}")
```

## Exception Details

All exceptions include:

| Attribute     | Type | Description                  |
| ------------- | ---- | ---------------------------- |
| `message`     | str  | Human-readable error message |
| `status_code` | int  | HTTP status code             |
| `body`        | dict | Full error response body     |

```python
try:
    job = client.get_job("99-9999.00")
except NotFoundError as e:
    print(e.message)      # "No job found: 99-9999.00"
    print(e.status_code)  # 404
    print(e.body)         # {"error": "job_not_found", "message": "...", "details": {...}}
```

## Exception Types

### AuthenticationError (401)

Raised when the API key is invalid or missing.

```python
try:
    client = CubitClient("invalid_key")
    client.me()
except AuthenticationError as e:
    print("Please check your API key")
    # Prompt user to re-enter key or check configuration
```

**Common causes:**

* Typo in API key
* Key was rotated/revoked
* Missing `Bearer` prefix (if using raw HTTP)

### AuthorizationError (403)

Raised when the API key lacks permission for the requested resource.

```python
try:
    # Semantic search requires Enterprise tier
    results = client.search_jobs_semantic("outdoor careers")
except AuthorizationError as e:
    print(f"Upgrade required: {e.body.get('upgrade_url')}")
    # Fall back to keyword search
    results = client.search_jobs("outdoor")
```

**Common causes:**

* Accessing Professional/Enterprise features on Sandbox tier
* Accessing an occupation not in Sandbox tier's allowed list

### NotFoundError (404)

Raised when the requested resource doesn't exist.

```python
try:
    job = client.get_job("99-9999.00")
except NotFoundError as e:
    soc_code = e.body.get("details", {}).get("soc_code")
    print(f"No job with SOC code: {soc_code}")
```

**Common causes:**

* Invalid SOC code format
* SOC code doesn't exist in O\*NET
* Invalid MSA code for regions

### ValidationError (422)

Raised when request parameters are invalid.

```python
try:
    results = client.search_jobs("", limit=1000)  # Empty query, limit too high
except ValidationError as e:
    print(f"Invalid request: {e.message}")
    print(e.body.get("details"))  # Shows which field failed
```

**Common causes:**

* Missing required parameters
* Parameter values out of range
* Invalid parameter types

### RateLimitError (429)

Raised when you've exceeded your rate limit.

```python
try:
    for soc in many_soc_codes:
        client.get_job(soc)
except RateLimitError as e:
    print(f"Rate limit exceeded")
    print(f"Retry after: {e.retry_after} seconds")
    print(f"Resets at: {e.body.get('rate_limit', {}).get('resets_at')}")
```

**Special attribute:**

* `retry_after`: Seconds to wait before retrying (from `Retry-After` header)

### ServerError (5xx)

Raised when the API server encounters an error.

```python
try:
    job = client.get_job("15-1252.00")
except ServerError as e:
    print(f"Server error: {e.status_code}")
    # Implement retry logic
    time.sleep(5)
    job = client.get_job("15-1252.00")
```

**Handling:**

* These are usually transient; retry after a delay
* If persistent, contact support

## Retry Patterns

### Simple Retry

```python
import time
from cubit import CubitClient, RateLimitError, ServerError

def get_job_with_retry(client, soc_code, max_retries=3):
    for attempt in range(max_retries):
        try:
            return client.get_job(soc_code)
        except RateLimitError as e:
            if attempt < max_retries - 1:
                wait = e.retry_after or 60
                print(f"Rate limited. Waiting {wait}s...")
                time.sleep(wait)
            else:
                raise
        except ServerError:
            if attempt < max_retries - 1:
                wait = 2 ** attempt  # Exponential backoff
                print(f"Server error. Retrying in {wait}s...")
                time.sleep(wait)
            else:
                raise
```

### Using tenacity

```python
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from cubit import CubitClient, RateLimitError, ServerError

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=1, max=60),
    retry=retry_if_exception_type((RateLimitError, ServerError))
)
def get_job_reliable(client, soc_code):
    return client.get_job(soc_code)
```

## Graceful Degradation

```python
from cubit import CubitClient, AuthorizationError, NotFoundError

def get_job_profile(client, soc_code):
    """Get job profile with graceful degradation."""
    try:
        # Try to get full profile with tasks
        job = client.get_job(soc_code)
        try:
            tasks = client.get_job_tasks(soc_code)
            job["tasks"] = tasks["tasks"]
        except AuthorizationError:
            # Sandbox tier - no task access
            job["tasks"] = None
            job["tasks_notice"] = "Upgrade to Professional for task-level analysis"
        return job
    except NotFoundError:
        return None
```

## Logging Errors

```python
import logging
from cubit import CubitClient, CubitError

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

client = CubitClient("cubit_key")

try:
    job = client.get_job("15-1252.00")
except CubitError as e:
    logger.error(
        "Cubit API error",
        extra={
            "status_code": e.status_code,
            "error_code": e.body.get("error"),
            "message": e.message
        }
    )
    raise
```

## Async Error Handling

Errors work identically with the async client:

```python
from cubit import CubitAsyncClient, NotFoundError

async with CubitAsyncClient("cubit_key") as client:
    try:
        job = await client.get_job("99-9999.00")
    except NotFoundError:
        print("Job not found")
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.maidenlabs.tools/sdk-reference/errors.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
