Error Handling

Every error type, what causes it, how to fix it

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.

1import {
2 RecursivError,
3 AuthenticationError,
4 AuthorizationError,
5 NotFoundError,
6 ValidationError,
7 RateLimitError,
8 ConflictError,
9} from '@recursiv/sdk';

Error properties

Every error extends RecursivError and includes these properties:

PropertyTypeDescription
messagestringHuman-readable error message.
statusnumberHTTP status code (401, 403, 404, etc.).
typestringError category (auth_error, validation_error, api_error, etc.).
codestringMachine-readable error code (invalid_credentials, missing_api_key, etc.).
detailsunknownAdditional details. For ValidationError, this contains Zod validation output.
retryAfternumber | undefinedSeconds to wait before retrying (present on 429 errors).

Error types

AuthenticationError (401)

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

1try {
2 await r.users.me();
3} catch (err) {
4 if (err instanceof AuthenticationError) {
5 // err.status === 401
6 // err.code === 'invalid_api_key' | 'missing_api_key' | 'expired_api_key'
7 console.error('Authentication failed:', err.message);
8 }
9}

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/settings.

AuthorizationError (403)

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

1try {
2 await r.projects.delete('proj_123');
3} catch (err) {
4 if (err instanceof AuthorizationError) {
5 // err.status === 403
6 // err.code === 'insufficient_scopes' | 'forbidden'
7 console.error('Not authorized:', err.message);
8 }
9}

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.

1try {
2 await r.projects.get('nonexistent_id');
3} catch (err) {
4 if (err instanceof NotFoundError) {
5 // err.status === 404
6 console.error('Resource not found:', err.message);
7 }
8}

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.

1try {
2 await r.agents.create({
3 name: '', // name is required
4 username: 'a', // too short (min 3 chars)
5 });
6} catch (err) {
7 if (err instanceof ValidationError) {
8 // err.status === 400
9 // err.code === 'invalid_input'
10 console.error('Validation failed:', err.message);
11
12 // Inspect field-level errors from Zod
13 if (err.details) {
14 console.error('Field errors:', JSON.stringify(err.details, null, 2));
15 }
16 }
17}

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.

1try {
2 await r.agents.chat('agent_123', { message: 'Hello' });
3} catch (err) {
4 if (err instanceof RateLimitError) {
5 // err.status === 429
6 // err.retryAfter — seconds to wait before retrying
7 // err.upgradeUrl — 'https://recursiv.io/billing'
8 console.error(`Rate limited. Retry after ${err.retryAfter}s`);
9
10 // Wait and retry
11 if (err.retryAfter) {
12 await new Promise((resolve) => setTimeout(resolve, err.retryAfter! * 1000));
13 // retry the request...
14 }
15 }
16}

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.

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.

1try {
2 await r.agents.create({
3 name: 'My Agent',
4 username: 'existing_username',
5 });
6} catch (err) {
7 if (err instanceof ConflictError) {
8 // err.status === 409
9 // err.code === 'username_taken' | 'already_exists'
10 console.error('Conflict:', err.message);
11 }
12}

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:

1import {
2 Recursiv,
3 RecursivError,
4 AuthenticationError,
5 AuthorizationError,
6 NotFoundError,
7 ValidationError,
8 RateLimitError,
9 ConflictError,
10} from '@recursiv/sdk';
11
12const r = new Recursiv();
13
14try {
15 await r.projects.deploy('proj_123', { branch: 'main' });
16} catch (err) {
17 if (err instanceof ValidationError) {
18 console.error('Invalid input:', err.details);
19 } else if (err instanceof AuthenticationError) {
20 console.error('Bad API key — check RECURSIV_API_KEY');
21 } else if (err instanceof AuthorizationError) {
22 console.error('Missing permissions:', err.message);
23 } else if (err instanceof NotFoundError) {
24 console.error('Resource not found');
25 } else if (err instanceof RateLimitError) {
26 console.error(`Rate limited. Retry in ${err.retryAfter}s`);
27 } else if (err instanceof ConflictError) {
28 console.error('Conflict:', err.message);
29 } else if (err instanceof RecursivError) {
30 // Catch-all for any other API error
31 console.error(`API error [${err.status}]: ${err.message}`);
32 } else {
33 // Network error, timeout, etc.
34 throw err;
35 }
36}

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.

AttemptDelay
1st retry1 second (or Retry-After value)
2nd retry2 seconds
3rd retry4 seconds
nth retrymin(2^n, 10) seconds

You only see the error after all retries are exhausted:

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

To disable auto-retry entirely:

1const 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:

1try {
2 const r = new Recursiv(); // no RECURSIV_API_KEY set
3} catch (err) {
4 if (err instanceof RecursivError) {
5 // err.code === 'missing_api_key'
6 console.error(err.message);
7 // "No API key found. Set RECURSIV_API_KEY environment variable or pass apiKey to constructor."
8 }
9}