---
title: SDK errors
description: >-
  IvyError hierarchy — IvyApiError, IvyWebSocketError, IvyTimeoutError with
  handling patterns
---

This page documents the error classes the SDK throws so you can catch and respond to failures precisely — distinguishing an expired auth token from a rate limit, a missing job, a dropped WebSocket, or a timed-out wait. Reach for it when you're writing `try`/`catch` logic or an `onError` handler and need to know which error type carries which recovery hint (for example, `IvyApiError.isRateLimited` or `isNotFound`). Every SDK error extends `IvyError`, so a single catch block can handle them all; the errors are raised by the [Client](/docs/api/sdk-client), [Jobs](/docs/api/sdk-jobs), and [WebSocket](/docs/api/sdk-websocket) calls documented alongside.

## SDK Errors Reference

> **Planned specification.** `@ivybiosciences/sdk` is not released, and there is no package to install. This page describes the SDK's planned interface. To call the platform today, use the [REST API](/docs/api/rest-jobs), the [ivybloom CLI](/docs/cli), or the [MCP server](/docs/cli/mcp).

All SDK errors extend `IvyError`, making it easy to catch any IvyAI-specific error in a single catch block while still distinguishing between API, WebSocket, and timeout errors.

```ts
import {
  IvyError,
  IvyApiError,
  IvyWebSocketError,
  IvyTimeoutError,
} from "@ivybiosciences/sdk"
```

---

## Error Hierarchy

```
Error (built-in)
└── IvyError
    ├── IvyApiError         (HTTP non-2xx responses)
    ├── IvyWebSocketError   (WebSocket connection failures)
    └── IvyTimeoutError     (Operation timeouts)
```

---

## `IvyError`

Base error class for all IvyAI SDK errors.

```ts
class IvyError extends Error {
  readonly code?: string
  constructor(message: string, code?: string)
}
```

| Property | Type | Description |
|----------|------|-------------|
| `message` | `string` | Human-readable error description |
| `code` | `string?` | Machine-readable error code (e.g., `"AUTH_REQUIRED"`) |
| `name` | `string` | Always `"IvyError"` |

### Examples

**TypeScript**

```ts
try {
  await ivy.jobs.submit(params)
} catch (e) {
  if (e instanceof IvyError) {
    // Any SDK error
    console.log(e.message, e.code)
  }
}
```

**Python**

```python
from requests.exceptions import HTTPError

try:
    response = requests.post(f"{BASE_URL}/api/v1/jobs", json=params, headers=headers)
    response.raise_for_status()
except HTTPError as e:
    print(f"API error: {e.response.status_code} — {e.response.json().get('detail')}")
```

**React**

```tsx
import { IvyError } from "@ivybiosciences/sdk"
import { useIvyJob } from "@ivybiosciences/sdk/react"

function SafeSubmit() {
  const { submit, error } = useIvyJob({
    onError: (e) => {
      if (e instanceof IvyError) {
        console.error(`[${e.code}] ${e.message}`)
      }
    },
  })

  return (
    <div>
      <button onClick={() => submit("esmfold", { protein_sequence: "..." })}>
        Submit
      </button>
      {error && <p className="text-red-600">{error.message}</p>}
    </div>
  )
}
```

---

## `IvyApiError`

Error thrown when the IvyAI API returns a non-2xx response. Provides structured access to the HTTP status code and convenience getters for common error categories.

```ts
class IvyApiError extends IvyError {
  readonly status: number
  readonly detail: string
  readonly body: unknown

  get isRateLimited(): boolean    // status === 429
  get isNotFound(): boolean       // status === 404
  get isUnauthorized(): boolean   // status === 401
  get isForbidden(): boolean      // status === 403
  get isServerError(): boolean    // status >= 500
}
```

### Constructor

```ts
new IvyApiError(response: {
  status?: number    // Defaults to 500
  detail?: string    // Defaults to "API error: {status}"
  body?: unknown     // Raw response body for debugging
})
```

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `status` | `number` | HTTP status code (400, 401, 404, 422, 429, 500, etc.) |
| `detail` | `string` | Error message from the API response body |
| `body` | `unknown` | Raw response body for debugging |

### Convenience Getters

| Getter | Returns `true` when | Recommended action |
|--------|--------------------|--------------------|
| `isRateLimited` | HTTP 429 | Back off and retry |
| `isNotFound` | HTTP 404 | Check resource ID |
| `isUnauthorized` | HTTP 401 | Re-authenticate |
| `isForbidden` | HTTP 403 | Check permissions |
| `isServerError` | HTTP 5xx | Retry (usually transient) |

### Examples

**TypeScript**

```ts
try {
  await ivy.jobs.submit({ toolName: "esmfold", parameters: {} })
} catch (e) {
  if (e instanceof IvyApiError) {
    console.log(e.status)        // 422
    console.log(e.detail)        // "Invalid protein sequence"
    console.log(e.isNotFound)    // false
    console.log(e.isRateLimited) // false
  }
}

// Rate limit handling
try {
  await ivy.jobs.submit(params)
} catch (e) {
  if (e instanceof IvyApiError && e.isRateLimited) {
    await new Promise(r => setTimeout(r, 60_000)) // Wait 1 minute
    await ivy.jobs.submit(params) // Retry
  }
}

// Not found handling
try {
  const status = await ivy.jobs.getStatus("job_nonexistent")
} catch (e) {
  if (e instanceof IvyApiError && e.isNotFound) {
    console.log("Job does not exist or has expired")
  }
}
```

**Python**

```python
from requests.exceptions import HTTPError

# Validation error
try:
    response = requests.post(f"{BASE_URL}/api/v1/jobs", json={
        "tool_name": "esmfold", "tool_input_parameters": {},
    }, headers=headers)
    response.raise_for_status()
except HTTPError as e:
    status_code = e.response.status_code
    detail = e.response.json().get("detail", "Unknown error")
    print(f"Status: {status_code}")  # 422
    print(f"Detail: {detail}")       # "Invalid protein sequence"

# Rate limit handling
try:
    response = requests.post(f"{BASE_URL}/api/v1/jobs", json=params, headers=headers)
    response.raise_for_status()
except HTTPError as e:
    if e.response.status_code == 429:
        import time
        time.sleep(60)  # Wait 1 minute
        requests.post(f"{BASE_URL}/api/v1/jobs", json=params, headers=headers)
```

**React**

```tsx
import { IvyApiError } from "@ivybiosciences/sdk"
import { useIvyJob } from "@ivybiosciences/sdk/react"

function ErrorAwareSubmit() {
  const { submit, error } = useIvyJob({
    onError: (e) => {
      if (e instanceof IvyApiError) {
        if (e.isRateLimited) toast.warn("Rate limited — wait and retry")
        else if (e.isNotFound) toast.error("Job not found or expired")
        else toast.error(`Error ${e.status}: ${e.detail}`)
      }
    },
  })

  return (
    <div>
      <button onClick={() => submit("esmfold", { protein_sequence: "..." })}>
        Submit
      </button>
      {error instanceof IvyApiError && (
        <p className="text-red-600">
          {error.isRateLimited ? "Too many requests" : error.detail}
        </p>
      )}
    </div>
  )
}
```

---

## `IvyWebSocketError`

Error thrown when a WebSocket connection fails or closes unexpectedly.

```ts
class IvyWebSocketError extends IvyError {
  readonly closeCode: number
  constructor(closeCode: number, reason: string)
}
```

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `closeCode` | `number` | The WebSocket close code |
| `message` | `string` | `"WebSocket closed: {reason} (code: {code})"` |

### Common Close Codes

| Code | Meaning | SDK Behavior |
|------|---------|--------------|
| `1000` | Normal closure | No reconnect |
| `1001` | Server going away (deploy) | Auto-reconnect |
| `1006` | Abnormal closure (network) | Auto-reconnect |
| `1008` | Policy violation (bad token) | Auto-reconnect (new token) |
| `4001` | Too many connections per user | No reconnect (close other connections) |
| `4002` | Too many connections per job | No reconnect (close other connections) |
| `4003` | Authentication failed | No reconnect (re-authenticate) |
| `4004` | Guest feature disabled | No reconnect |

### Examples

**TypeScript**

```ts
import { IvyWebSocketError } from "@ivybiosciences/sdk"

try {
  await ivy.websocket.connect("job_abc123")
} catch (e) {
  if (e instanceof IvyWebSocketError) {
    console.log(`Close code: ${e.closeCode}`)
    if (e.closeCode === 4003) {
      // Authentication issue — token may have expired
    }
  }
}
```

**Python**

```python
import httpx

def safe_follow(job_id: str, api_key: str):
    # Today: the SSE job event proxy (there is no public WebSocket)
    url = f"https://www.ivybiosciences.com/api/v1/jobs/{job_id}/events"
    headers = {"Authorization": f"Bearer {api_key}"}
    try:
        with httpx.stream("GET", url, headers=headers, timeout=None) as response:
            response.raise_for_status()
            for line in response.iter_lines():
                print(line)
    except httpx.HTTPStatusError as e:
        if e.response.status_code == 401:
            print("Authentication failed — the key or token is missing or expired")
        elif e.response.status_code == 403:
            print("This credential cannot read the job")

safe_follow("job_abc123", "ivy_your_key_here")
```

**React**

```tsx
import { useIvyWebSocket } from "@ivybiosciences/sdk/react"

function SafeMonitor({ jobId }: { jobId: string }) {
  // The hook reports close failures through `error`; it takes no onClose option.
  const { status, isConnected, error } = useIvyWebSocket(jobId)

  if (error) return <p className="text-red-600">Connection error: {error.message}</p>
  if (!isConnected) return <p>Connecting...</p>

  return <p>{status?.phase}: {status?.progress}%</p>
}
```

---

## `IvyTimeoutError`

Error thrown when an operation exceeds its configured timeout. Most commonly occurs during `submitAndWait()`.

```ts
class IvyTimeoutError extends IvyError {
  readonly timeoutMs: number
  constructor(timeoutMs: number)
}
```

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `timeoutMs` | `number` | The timeout value that was exceeded, in milliseconds |
| `message` | `string` | `"Operation timed out after {timeoutMs}ms"` |

### Examples

**TypeScript**

```ts
import { IvyTimeoutError } from "@ivybiosciences/sdk"

try {
  await ivy.jobs.submitAndWait(params, { timeout: 30_000 }) // 30s
} catch (e) {
  if (e instanceof IvyTimeoutError) {
    console.log(`Timed out after ${e.timeoutMs}ms`)
    // The job is still running server-side — check status manually
  }
}
```

**Python**

```python
import time

# Implement a manual timeout for polling
job = requests.post(f"{BASE_URL}/api/v1/jobs", json=params, headers=headers).json()

timeout_sec = 30
start = time.time()

while time.time() - start &lt; timeout_sec:
    status = requests.get(
        f"{BASE_URL}/api/v1/jobs/{job['id']}", headers=headers
    ).json()
    if status["status"] in ("COMPLETED", "FAILURE"):
        break
    time.sleep(3)
else:
    elapsed = time.time() - start
    print(f"Timed out after {elapsed:.0f}s — job still running server-side")
    # Check status later with: GET /api/v1/jobs/{job_id}
```

**React**

```tsx
import { IvyTimeoutError } from "@ivybiosciences/sdk"
import { useIvyJob } from "@ivybiosciences/sdk/react"
import { useState } from "react"

function TimeoutAwareSubmit() {
  const [timedOut, setTimedOut] = useState(false)
  const [jobId, setJobId] = useState<string | null>(null)

  const { submit, error } = useIvyJob({
    onError: (e) => {
      if (e instanceof IvyTimeoutError) {
        setTimedOut(true)
        // Job is still running — user can check status later
      }
    },
  })

  return (
    <div>
      <button onClick={async () => setJobId(await submit("esmfold", { protein_sequence: "..." }))}>
        Submit
      </button>
      {timedOut && (
        <p>
          Job {jobId} is still running. Check back later for results.
        </p>
      )}
    </div>
  )
}
```

---

## Error Handling Patterns

### Catch-All SDK Errors

**TypeScript**

```ts
try {
  const result = await ivy.jobs.submitAndWait(params)
} catch (e) {
  if (e instanceof IvyError) {
    // Any SDK error — log and display
    console.error(`[${e.name}] ${e.message}`)
  } else {
    // Non-SDK error (network, runtime, etc.)
    throw e
  }
}
```

**Python**

```python
from requests.exceptions import HTTPError, ConnectionError, Timeout

try:
    response = requests.post(f"{BASE_URL}/api/v1/jobs", json=params, headers=headers)
    response.raise_for_status()
except HTTPError as e:
    print(f"[API Error] {e.response.status_code}: {e.response.json().get('detail')}")
except (ConnectionError, Timeout) as e:
    print(f"[Network Error] {e}")
```

**React**

```tsx
import { IvyError } from "@ivybiosciences/sdk"
import { useIvyJob } from "@ivybiosciences/sdk/react"

function CatchAllExample() {
  const { submit, error } = useIvyJob({
    onError: (e) => {
      if (e instanceof IvyError) {
        toast.error(`[${e.name}] ${e.message}`)
      } else {
        toast.error("An unexpected error occurred")
      }
    },
  })

  return <button onClick={() => submit("esmfold", { protein_sequence: "..." })}>Run</button>
}
```

### Comprehensive Error Handling

**TypeScript**

```ts
try {
  const result = await ivy.jobs.submitAndWait(params, { timeout: 120_000 })
  // Handle success...
} catch (e) {
  if (e instanceof IvyApiError) {
    if (e.isRateLimited) {
      showToast("Rate limited — please wait and try again")
    } else if (e.isUnauthorized) {
      redirectToLogin()
    } else if (e.isNotFound) {
      showToast("Resource not found")
    } else if (e.isServerError) {
      showToast("Server error — retrying automatically")
    } else {
      showToast(`Error: ${e.detail}`)
    }
  } else if (e instanceof IvyTimeoutError) {
    showToast(`Job timed out after ${e.timeoutMs / 1000}s`)
  } else if (e instanceof IvyWebSocketError) {
    showToast("Connection lost — using polling fallback")
  }
}
```

**Python**

```python
from requests.exceptions import HTTPError, ConnectionError, Timeout

try:
    response = requests.post(
        f"{BASE_URL}/api/v1/jobs", json=params, headers=headers, timeout=120
    )
    response.raise_for_status()
    job = response.json()

    import time
    start = time.time()
    while time.time() - start &lt; 120:
        status = requests.get(
            f"{BASE_URL}/api/v1/jobs/{job['id']}", headers=headers
        ).json()
        if status["status"] in ("COMPLETED", "FAILURE"):
            break
        time.sleep(3)
    else:
        print("Job timed out — still running server-side")

except HTTPError as e:
    code = e.response.status_code
    detail = e.response.json().get("detail", "Unknown")
    if code == 429:
        print("Rate limited — wait and try again")
    elif code == 401:
        print("Unauthorized — re-authenticate")
    elif code == 404:
        print("Resource not found")
    elif code &ge; 500:
        print("Server error — retrying automatically")
    else:
        print(f"Error: {detail}")
except Timeout:
    print("Request timed out")
except ConnectionError:
    print("Connection lost — check network")
```

**React**

```tsx
import { IvyApiError, IvyTimeoutError, IvyWebSocketError } from "@ivybiosciences/sdk"
import { useIvyJob } from "@ivybiosciences/sdk/react"

function ComprehensiveErrorHandling() {
  const { submit, error } = useIvyJob({
    onError: (e) => {
      if (e instanceof IvyApiError) {
        if (e.isRateLimited) toast.warn("Rate limited — please wait")
        else if (e.isUnauthorized) router.push("/login")
        else if (e.isNotFound) toast.error("Resource not found")
        else if (e.isServerError) toast.error("Server error — retrying")
        else toast.error(`Error: ${e.detail}`)
      } else if (e instanceof IvyTimeoutError) {
        toast.warn(`Timed out after ${e.timeoutMs / 1000}s`)
      } else if (e instanceof IvyWebSocketError) {
        toast.info("Connection lost — using polling fallback")
      }
    },
  })

  return (
    <button onClick={() => submit("diffdock", { protein_pdb: "...", ligand_smiles: "CCO" })}>
      Run Docking
    </button>
  )
}
```

---

## Harness Error Codes

The [Harness Sessions API](/docs/api/sessions-overview) returns structured error codes with machine-readable identifiers and recovery hints. These are separate from HTTP status codes.

```ts
interface HarnessError {
  error: string        // Human-readable message
  code: string         // Machine-readable code (e.g., "SESSION_NOT_FOUND")
  recoveryHint?: string // Suggested fix
}
```

### Error Code Ranges

| Range | Category | Description |
|-------|----------|-------------|
| 1xxx | Session | Session lifecycle errors |
| 2xxx | Storage | Database and persistence errors |
| 3xxx | Checkpoint | Checkpoint and rollback errors |
| 4xxx | Sync | Version vector and conflict errors |
| 5xxx | Tool | Tool execution errors |
| 6xxx | Auth | Authentication and authorization errors |
| 7xxx | Rate Limit | Rate limiting and quota errors |
| 8xxx | Streaming | SSE stream errors |
| 9xxx | Extension | Enterprise extension errors |

### Common Error Codes

| Code | Name | HTTP | Recovery |
|------|------|:---:|----------|
| `1001` | `SESSION_NOT_FOUND` | 404 | Create a new session |
| `1002` | `SESSION_ARCHIVED` | 409 | Unarchive or create new session |
| `1003` | `SESSION_LOCKED` | 423 | Wait for current execution to finish |
| `2001` | `STORAGE_UNAVAILABLE` | 503 | Retry after backoff |
| `2002` | `STORAGE_WRITE_FAILED` | 500 | Retry; check database connectivity |
| `3001` | `CHECKPOINT_NOT_FOUND` | 404 | List checkpoints to find valid ID |
| `3002` | `CHECKPOINT_EXPIRED` | 410 | Checkpoints beyond retention are pruned |
| `4001` | `SYNC_CONFLICT` | 409 | Re-fetch session state and retry |
| `4002` | `SYNC_VERSION_MISMATCH` | 409 | Pull latest version vector first |
| `5001` | `TOOL_NOT_FOUND` | 404 | Check tool name against `/api/harness/tools` |
| `5002` | `TOOL_EXECUTION_FAILED` | 502 | Retry; check tool parameters |
| `5003` | `TOOL_TIMEOUT` | 504 | Increase timeout or simplify parameters |
| `6001` | `AUTH_REQUIRED` | 401 | Provide API key or JWT |
| `6002` | `AUTH_EXPIRED` | 401 | Refresh JWT token |
| `6003` | `INSUFFICIENT_PERMISSIONS` | 403 | Check API key scopes |
| `7001` | `RATE_LIMITED` | 429 | Wait for `Retry-After` header duration |
| `7002` | `QUOTA_EXCEEDED` | 429 | Upgrade plan or wait for quota reset |
| `8001` | `STREAM_INTERRUPTED` | 502 | Reconnect and resume from last event |
| `9001` | `EXTENSION_NOT_FOUND` | 404 | Check enterprise extension configuration |

### Handling Harness Errors

```ts
const response = await fetch(`/api/harness/sessions/${id}/execute`, {
  method: "POST",
  headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
  body: JSON.stringify({ content: message }),
})

if (!response.ok) {
  const error = await response.json()
  // error.code is the machine-readable code
  // error.recoveryHint suggests how to fix it
  switch (error.code) {
    case "SESSION_LOCKED":
      // Wait and retry
      break
    case "SYNC_CONFLICT":
      // Re-fetch and merge
      break
    case "RATE_LIMITED":
      const retryAfter = response.headers.get("Retry-After")
      // Wait retryAfter seconds
      break
  }
}
```

---

### Automatic Retry (Built-in)

The SDK automatically retries transient errors (429, 500, 502, 503, 504) with exponential backoff. You only receive these errors after all retries are exhausted.

```ts
const ivy = createIvyClient({
  apiKey: "ivy_...",
  retries: 5, // Try up to 5 times before throwing
})
```

The retry formula: `baseDelay * 2^attempt + random_jitter`
