> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.recursiv.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.recursiv.io/_mcp/server.

# Error Handling

## Overview

Every error thrown by the SDK is an instance of `RecursivError` or one of its subclasses. All errors include structured metadata so you can handle them programmatically.

```typescript
import {
  RecursivError,
  AuthenticationError,
  AuthorizationError,
  NotFoundError,
  ValidationError,
  RateLimitError,
  ConflictError,
} from '@recursiv/sdk';
```

## Error properties

Every error extends `RecursivError` and includes these properties:

| Property     | Type                  | Description                                                                     |
| ------------ | --------------------- | ------------------------------------------------------------------------------- |
| `message`    | `string`              | Human-readable error message.                                                   |
| `status`     | `number`              | HTTP status code (401, 403, 404, etc.).                                         |
| `type`       | `string`              | Error category (`auth_error`, `validation_error`, `api_error`, etc.).           |
| `code`       | `string`              | Machine-readable error code (`invalid_credentials`, `missing_api_key`, etc.).   |
| `details`    | `unknown`             | Additional details. For `ValidationError`, this contains Zod validation output. |
| `retryAfter` | `number \| undefined` | Seconds to wait before retrying (present on 429 errors).                        |

## Error types

### AuthenticationError (401)

Thrown when the API key is missing, invalid, or expired.

```typescript
try {
  await r.users.me();
} catch (err) {
  if (err instanceof AuthenticationError) {
    // err.status === 401
    // err.code === 'invalid_api_key' | 'missing_api_key' | 'expired_api_key'
    console.error('Authentication failed:', err.message);
  }
}
```

**Common causes:**

* API key not set in environment or constructor
* API key was revoked or rotated
* Using a session token where an API key is expected

**Fix:** Check that `RECURSIV_API_KEY` is set and valid. Generate a new key at [recursiv.io/account/api-keys](https://recursiv.io/account/api-keys).

### AuthorizationError (403)

Thrown when the API key is valid but lacks the required scopes or permissions.

```typescript
try {
  await r.projects.delete('proj_123');
} catch (err) {
  if (err instanceof AuthorizationError) {
    // err.status === 403
    // err.code === 'insufficient_scopes' | 'forbidden'
    console.error('Not authorized:', err.message);
  }
}
```

**Common causes:**

* API key does not have the required scope (e.g., `projects:write`)
* Trying to access a resource owned by another user or organization
* Organization membership required

**Fix:** Create a new API key with the necessary scopes, or check that you have the correct organization role.

### NotFoundError (404)

Thrown when the requested resource does not exist.

```typescript
try {
  await r.projects.get('nonexistent_id');
} catch (err) {
  if (err instanceof NotFoundError) {
    // err.status === 404
    console.error('Resource not found:', err.message);
  }
}
```

**Common causes:**

* ID does not exist
* Resource was deleted
* Typo in the resource ID

### ValidationError (400)

Thrown when the request body fails validation. The `details` property contains the Zod validation output with per-field errors.

```typescript
try {
  await r.agents.create({
    name: '',      // name is required
    username: 'a', // too short (min 3 chars)
  });
} catch (err) {
  if (err instanceof ValidationError) {
    // err.status === 400
    // err.code === 'invalid_input'
    console.error('Validation failed:', err.message);

    // Inspect field-level errors from Zod
    if (err.details) {
      console.error('Field errors:', JSON.stringify(err.details, null, 2));
    }
  }
}
```

**Common causes:**

* Missing required fields
* Invalid field types or formats
* Values outside allowed ranges
* String length violations

**Fix:** Check `err.details` for the specific field that failed validation.

### RateLimitError (429)

Thrown when you exceed the rate limit for your plan. Includes `retryAfter` (seconds) and an `upgradeUrl` for free-tier users.

```typescript
try {
  await r.agents.chat('agent_123', { message: 'Hello' });
} catch (err) {
  if (err instanceof RateLimitError) {
    // err.status === 429
    // err.retryAfter — seconds to wait before retrying
    // err.upgradeUrl — 'https://recursiv.io/billing'
    console.error(`Rate limited. Retry after ${err.retryAfter}s`);

    // Wait and retry
    if (err.retryAfter) {
      await new Promise((resolve) => setTimeout(resolve, err.retryAfter! * 1000));
      // retry the request...
    }
  }
}
```

**Common causes:**

* Too many requests in a short window
* Free-tier daily limits exceeded
* Agent request count exhausted

**Fix:** Wait for `retryAfter` seconds, or upgrade your plan at `err.upgradeUrl`.

> **Note**
>
> The SDK automatically retries 429 errors with exponential backoff (up to `maxRetries` times). You only see this error if all retries are exhausted.

### ConflictError (409)

Thrown when the request conflicts with the current state of a resource.

```typescript
try {
  await r.agents.create({
    name: 'My Agent',
    username: 'existing_username',
  });
} catch (err) {
  if (err instanceof ConflictError) {
    // err.status === 409
    // err.code === 'username_taken' | 'already_exists'
    console.error('Conflict:', err.message);
  }
}
```

**Common causes:**

* Username already taken
* Slug already in use
* Duplicate resource creation
* Dispatcher task already claimed by another agent

## Generic error handler

A catch-all pattern that handles every error type:

```typescript
import {
  Recursiv,
  RecursivError,
  AuthenticationError,
  AuthorizationError,
  NotFoundError,
  ValidationError,
  RateLimitError,
  ConflictError,
} from '@recursiv/sdk';

const r = new Recursiv();

try {
  await r.projects.deploy('proj_123', { branch: 'main' });
} catch (err) {
  if (err instanceof ValidationError) {
    console.error('Invalid input:', err.details);
  } else if (err instanceof AuthenticationError) {
    console.error('Bad API key — check RECURSIV_API_KEY');
  } else if (err instanceof AuthorizationError) {
    console.error('Missing permissions:', err.message);
  } else if (err instanceof NotFoundError) {
    console.error('Resource not found');
  } else if (err instanceof RateLimitError) {
    console.error(`Rate limited. Retry in ${err.retryAfter}s`);
  } else if (err instanceof ConflictError) {
    console.error('Conflict:', err.message);
  } else if (err instanceof RecursivError) {
    // Catch-all for any other API error
    console.error(`API error [${err.status}]: ${err.message}`);
  } else {
    // Network error, timeout, etc.
    throw err;
  }
}
```

## Auto-retry behavior

The SDK automatically retries requests on **429** and **5xx** status codes using exponential backoff. It respects the `Retry-After` header when present.

| Attempt   | Delay                             |
| --------- | --------------------------------- |
| 1st retry | 1 second (or `Retry-After` value) |
| 2nd retry | 2 seconds                         |
| 3rd retry | 4 seconds                         |
| nth retry | `min(2^n, 10)` seconds            |

You only see the error after all retries are exhausted:

```typescript
const r = new Recursiv({
  maxRetries: 3, // 1 initial request + 3 retries = 4 total attempts
});
```

To disable auto-retry entirely:

```typescript
const r = new Recursiv({ maxRetries: 0 });
```

## Configuration errors

If the SDK cannot find an API key during construction (and `anonymous` mode is not enabled), it throws immediately:

```typescript
try {
  const r = new Recursiv(); // no RECURSIV_API_KEY set
} catch (err) {
  if (err instanceof RecursivError) {
    // err.code === 'missing_api_key'
    console.error(err.message);
    // "No API key found. Set RECURSIV_API_KEY environment variable or pass apiKey to constructor."
  }
}
```