# Debugging Cursor SDK Error Handling and Retry Logic: Patterns and Best Practices

> Master Cursor SDK error handling and retry logic. Learn best practices for startup vs runtime failures in your plugins for robust application performance.

- Repository: [Cursor/plugins](https://github.com/cursor/plugins)
- Tags: best-practices
- Published: 2026-05-25

---

**The Cursor SDK distinguishes between startup failures (caught as `CursorAgentError` with an `isRetryable` flag) and run-time failures (surfaced via `RunResult.status === "error"`), requiring different handling strategies where only startup errors support automatic retry with exponential back-off.**

The Cursor SDK provides a robust TypeScript client for orchestrating AI agents, but implementing resilient integrations requires understanding its bifurcated error model. This guide examines the source code in the `cursor/plugins` repository to explain how to handle transient network failures, rate limits, and run-time execution errors using the `CursorAgentError` hierarchy and `isRetryable` semantics according to the official reference documentation.

## Understanding the Two Failure Axes

The SDK categorizes failures across two distinct axes that determine retry eligibility. According to [`cursor-sdk/skills/cursor-sdk/references/error-handling.md`](https://github.com/cursor/plugins/blob/main/cursor-sdk/skills/cursor-sdk/references/error-handling.md), conflating these categories leads to infinite retry loops or missed transient failures.

### Startup Errors

Startup errors occur when the agent cannot initialize due to authentication failures, configuration issues, or network problems. These throw instances of `CursorAgentError` or its subclasses (`AuthenticationError`, `RateLimitError`, `NetworkError`). Each error object carries an `isRetryable` boolean flag that the backend populates to indicate whether the condition is transient.

### Run-time Errors

Run-time errors happen after the agent successfully starts and performs work. These do **not** throw exceptions. Instead, the promise returned by `run.wait()` resolves to a `RunResult` object where `status === "error"`. The SDK documentation explicitly warns against automatically retrying these failures; instead, you should log the run ID and inspect the conversation via `Agent.getRun` before deciding on a human-driven retry.

## Handling Startup Failures with CursorAgentError

All SDK-thrown errors inherit from the `CursorAgentError` base class defined in the Cursor SDK core. Sub-classes provide specific failure classification, while the `isRetryable` property serves as the authoritative signal for retry decisions.

### The isRetryable Flag

Do **not** infer retryability from the error type alone. Always check the `isRetryable` boolean that the backend supplies. As noted in the error handling reference, even `NetworkError` instances may have `isRetryable === false` in certain authentication contexts.

Key properties available on every `CursorAgentError`:

- `message` – Human-readable description
- `code` – SDK error code string
- `protoErrorCode` – Protocol-level error code
- `isRetryable` – Boolean flag indicating transient status
- `cause` – Optional underlying error

### Exponential Back-off Implementation

For transient startup errors (`isRetryable === true`), implement exponential back-off with jitter and limit attempts to **3 or fewer** for cloud agents. The following pattern from [`cursor-sdk/skills/cursor-sdk/references/error-handling.md`](https://github.com/cursor/plugins/blob/main/cursor-sdk/skills/cursor-sdk/references/error-handling.md) demonstrates the recommended approach:

```typescript
import { Agent, CursorAgentError } from "@cursor/sdk";

async function runWithRetry() {
  const maxAttempts = 3;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      await using agent = Agent.create({
        apiKey: process.env.CURSOR_API_KEY!,
        model: { id: "composer-2" },
        cloud: { repos: [{ url: "https://github.com/your-org/repo", startingRef: "main" }] },
      });
      
      const run = await agent.send("Summarize the repository.");
      const result = await run.wait();
      return result;
      
    } catch (err) {
      if (err instanceof CursorAgentError && err.isRetryable) {
        if (attempt === maxAttempts) throw err;
        const backoff = 2 ** attempt * 1000 + Math.random() * 500;
        await new Promise(r => setTimeout(r, backoff));
      } else {
        throw err;
      }
    }
  }
}

```

## Managing Run-time Errors and RunResult Status

Once the agent starts successfully, failures surface through the status property rather than exceptions. Your code must handle three possible statuses: `"finished"`, `"cancelled"`, and `"error"`.

When `result.status === "error"`, extract the run ID from `result.id` and surface it for human triage. The orchestrator implementation in [`orchestrate/skills/orchestrate/scripts/core/loop.ts`](https://github.com/cursor/plugins/blob/main/orchestrate/skills/orchestrate/scripts/core/loop.ts) demonstrates this pattern by writing a handoff record rather than retrying:

```typescript
if (result.status === "error") {
  console.error(`[task] ${task.name} failed – run ${result.id}`);
  await writeHandoff(task, {
    failureMode: "tool-error",
    sdkError: result.error?.message ?? "unknown",
  });
}

```

## Practical Implementation Patterns

### Retry Helper for Agent Creation

Encapsulate the retry logic in a reusable helper that specifically filters for transient network conditions. This implementation from the error handling documentation targets `NetworkError` and `RateLimitError` while respecting the `isRetryable` flag:

```typescript
import { Agent, CursorAgentError, NetworkError, RateLimitError } from "@cursor/sdk";

export async function createWithRetry(opts: Parameters<typeof Agent.create>[0]) {
  const maxAttempts = 3;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await Agent.create(opts);
    } catch (e) {
      const retryable =
        e instanceof CursorAgentError &&
        e.isRetryable &&
        (e instanceof NetworkError || e instanceof RateLimitError);
      
      if (!retryable || attempt === maxAttempts) throw e;
      
      const backoff = 2 ** attempt * 1000 + Math.random() * 500;
      await new Promise(r => setTimeout(r, backoff));
    }
  }
  throw new Error("unreachable");
}

```

### CI/CD Exit Code Conventions

The SDK defines specific exit codes to signal execution outcomes to CI/CD pipelines. As documented in [`cursor-sdk/skills/cursor-sdk/references/patterns.md`](https://github.com/cursor/plugins/blob/main/cursor-sdk/skills/cursor-sdk/references/patterns.md), use these conventions:

- **0** – Success (`result.status === "finished"`)
- **1** – Permanent startup failure (non-retryable `CursorAgentError`)
- **2** – Run finished with `status: "error"`
- **75** – Transient retryable failure (POSIX `EX_TEMPFAIL`)

The following GitHub Action pattern demonstrates proper exit code handling:

```typescript
import { Agent, CursorAgentError } from "@cursor/sdk";

async function main() {
  await using agent = Agent.create({ /* …options… */ });
  
  try {
    const run = await agent.send("Review the PR …");
    const result = await run.wait();
    
    if (result.status !== "finished") {
      console.error(`run ended as ${result.status}`);
      process.exit(2);
    }
    console.log("✅ review complete");
    
  } catch (err) {
    if (err instanceof CursorAgentError) {
      console.error(`startup failed: ${err.message}`);
      process.exit(err.isRetryable ? 75 : 1);
    }
    throw err;
  }
}

```

## Orchestrator Integration Patterns

For sophisticated workflows using the orchestrator skills, failure modes drive planner decisions through handoff specifications. The [`orchestrate/skills/orchestrate/scripts/handoffs.md`](https://github.com/cursor/plugins/blob/main/orchestrate/skills/orchestrate/scripts/handoffs.md) file defines categories including `cap-hit`, `oom`, `network-drop`, `tool-error`, and `unknown` that determine whether the planner should retry, back-off, or abandon the task.

When integrating with the orchestrator loop in [`orchestrate/skills/orchestrate/scripts/core/loop.ts`](https://github.com/cursor/plugins/blob/main/orchestrate/skills/orchestrate/scripts/core/loop.ts), never auto-retry run-time errors. Instead, populate the handoff structure with the failure mode and let the planner's hygiene logic (defined in [`orchestrate/skills/orchestrate/prompts/loop-hygiene.md`](https://github.com/cursor/plugins/blob/main/orchestrate/skills/orchestrate/prompts/loop-hygiene.md)) decide the next action.

## Summary

- **Distinguish failure types**: Catch `CursorAgentError` for startup issues; check `RunResult.status` for run-time failures.
- **Respect `isRetryable`**: Always rely on this boolean flag rather than inferring retryability from error class names.
- **Limit startup retries**: Use exponential back-off with jitter and cap attempts at 3 for cloud agents.
- **Never auto-retry run-time errors**: Log the run ID and surface dashboard links for human triage when `result.status === "error"`.
- **Use standard exit codes**: Return 75 for transient retryable failures, 1 for permanent startup failures, and 2 for run-time errors to integrate cleanly with CI/CD systems.

## Frequently Asked Questions

### How do I differentiate between a retryable network error and a permanent authentication failure?

Both errors extend `CursorAgentError`, so you must inspect the `isRetryable` property rather than using `instanceof` checks alone. According to the Cursor SDK error handling reference, an `AuthenticationError` typically has `isRetryable === false`, while a `NetworkError` usually has `isRetryable === true`, though the backend ultimately controls this flag based on context.

### What is the maximum number of retries recommended for Cursor SDK startup errors?

The documentation recommends **3 or fewer attempts** for cloud agents when handling transient startup failures. Implement exponential back-off with jitter (e.g., `2 ** attempt * 1000 + Math.random() * 500` milliseconds) to avoid overwhelming the API during recovery periods.

### Why should I not automatically retry when `RunResult.status === "error"`?

Run-time errors indicate the agent performed work but failed during execution, potentially consuming tokens or modifying state. Blindly retrying could duplicate expensive operations or amplify failures. Instead, log the `result.id` and surface a dashboard link for human inspection via `Agent.getRun` before deciding whether to retry.

### Which exit code should my CI pipeline use for a transient rate limit error?

Exit with code **75** (POSIX `EX_TEMPFAIL`) to signal that the failure is transient and the job should be retried by the CI scheduler. For permanent startup failures, use code 1; for successful runs that encountered execution errors, use code 2.