> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Error handling

> Handle HTTP, connection, timeout, and job errors from ndi-sdk

Every error raised by `ndi-sdk` extends `NdiError`. Check a specific class when
your application can recover from that failure, or check `NdiError` at the
outer SDK boundary.

## Error hierarchy

```text theme={"dark"}
NdiError
├── NdiConnectionError
│   └── NdiTimeoutError
├── NdiStatusError
│   ├── AuthenticationError       // 401
│   ├── QuotaExceededError        // 402
│   ├── PermissionDeniedError     // 403
│   ├── NotFoundError             // 404
│   ├── SyncWaitTimeoutError      // 408, legacy API only
│   ├── ConflictError             // 409
│   ├── ResultExpiredError        // 410
│   ├── RateLimitError            // 429
│   ├── NotImplementedByServerError // 501
│   ├── ServerError               // other 5xx
│   └── InvalidRequestError       // other non-2xx responses
├── JobFailedError
└── JobTimeoutError
```

## Handle common failures

```ts theme={"dark"}
import {
  AuthenticationError,
  JobFailedError,
  JobTimeoutError,
  NdiClient,
  NdiConnectionError,
  NdiStatusError,
} from "ndi-sdk";

const client = new NdiClient();

try {
  const upload = await client.documents.createUpload("report.pdf");
  const queued = await client.documents.parse(upload);
  const job = await client.jobs.wait(queued.job_id, { timeout: 600 });
  console.log(job.result);
} catch (err) {
  if (err instanceof AuthenticationError) {
    console.error("Check NDI_API_KEY", err.request_id);
  } else if (err instanceof JobFailedError) {
    console.error(err.job.error);
  } else if (err instanceof JobTimeoutError) {
    console.log(`Job continues running: ${err.job.job_id}`);
  } else if (err instanceof NdiConnectionError) {
    console.error(`Could not reach NDI: ${err.message}`);
  } else if (err instanceof NdiStatusError) {
    console.error(err.status_code, err.code, err.message, err.request_id);
  } else {
    throw err;
  }
}
```

## HTTP status errors

`NdiStatusError` means the server returned a non-success HTTP response. It
exposes:

| Property         | Meaning                                                       |
| ---------------- | ------------------------------------------------------------- |
| `status_code`    | HTTP status                                                   |
| `code`           | Machine-readable `ErrorCode`, or `null` for a legacy response |
| `message`        | Human-readable server message                                 |
| `body`           | Parsed `/v1` error body when available                        |
| `retryable`      | Whether replaying the identical request could succeed         |
| `request_id`     | Correlation ID for logs and support                           |
| `method` / `url` | Failed request                                                |

Handle error codes when behavior differs within one HTTP status:

```ts theme={"dark"}
import { ErrorCode, RateLimitError } from "ndi-sdk";

try {
  const queued = await client.documents.parse(upload);
} catch (err) {
  if (err instanceof RateLimitError) {
    if (err.code === ErrorCode.CONCURRENCY_LIMIT_REACHED) {
      console.log("Too many jobs are already in flight");
    } else if (err.code === ErrorCode.RATE_LIMITED) {
      console.log("Request rate limit reached");
    }
  }
  throw err;
}
```

`ErrorCode` is open-ended. Unknown codes remain usable strings instead of
causing response parsing to fail.

`unsupported_file_type` is an `InvalidRequestError` with status `422`. It is
returned synchronously by an upload boundary or document-operation start, so
there is no failed job to poll. A rejected workspace upload also creates no
file. Check `err.code === ErrorCode.UNSUPPORTED_FILE_TYPE`; do not retry the
same request or maintain a separate client-side format allowlist.

## Job errors are different

An HTTP request can succeed while the asynchronous job later fails.
`JobFailedError` therefore contains the terminal `job`, including
`job.error.code`, `job.error.message`, and any job-specific detail.

```ts theme={"dark"}
import { JobFailedError } from "ndi-sdk";

const jobId = "550e8400-e29b-41d4-a716-446655440003";

try {
  const job = await client.jobs.wait(jobId);
} catch (err) {
  if (err instanceof JobFailedError) {
    console.error(err.job.error?.code, err.job.error?.message);
  } else {
    throw err;
  }
}
```

`JobTimeoutError` only means the local `jobs.wait` budget expired. The job
continues server-side and can be read again with `client.jobs.get(jobId)`.

## Connection and HTTP timeouts

`NdiConnectionError` means no response was produced because of DNS, TLS,
connection, or transport failure. `NdiTimeoutError` is its timeout-specific
subclass.

The client timeout defaults to 60 seconds per HTTP request:

```ts theme={"dark"}
import { NdiClient, NdiTimeoutError } from "ndi-sdk";

const client = new NdiClient({ timeout: 120 });
const jobId = "550e8400-e29b-41d4-a716-446655440003";

try {
  const job = await client.jobs.get(jobId);
} catch (err) {
  if (err instanceof NdiTimeoutError) {
    console.log("The HTTP request timed out; the remote job may still be running");
  } else {
    throw err;
  }
}
```

This HTTP timeout is separate from
`client.jobs.wait(jobId, { timeout: 600 })`, which sets the total polling
budget.

## Automatic retries

The SDK retries safe requests after connection failures, HTTP `429`, and
`5xx` responses. The default retry policy makes at most three attempts with
exponential backoff and full jitter. The defaults are `initial_backoff: 0.5`
seconds and `max_backoff: 8` seconds. A valid `Retry-After` response header
takes precedence.

```ts theme={"dark"}
import { NdiClient, RetryPolicy } from "ndi-sdk";

const client = new NdiClient({
  retry_policy: RetryPolicy({ max_attempts: 5 }),
});

const jobId = "550e8400-e29b-41d4-a716-446655440003";
const job = await client.jobs.get(jobId);
```

The SDK only retries a request when replay is safe. Read methods are
idempotent, and job-creating methods automatically mint an
`Idempotency-Key`. State conflicts (`409`) and invalid requests are not
retried. Streaming `jobs.events` connections are not retried automatically;
reconnect with `last_event_id` when needed.

## Best practices

<CardGroup cols={2}>
  <Card title="Check specific error classes" icon="code">
    Recover from known conditions such as rate limits or missing resources,
    then rethrow unexpected values.
  </Card>

  <Card title="Log the request ID" icon="file-text">
    Include `request_id`, `status_code`, and `code` in logs without recording
    API keys or document contents.
  </Card>

  <Card title="Distinguish HTTP and job failure" icon="split">
    A successful submission can still produce a failed job. Handle both
    `NdiStatusError` and `JobFailedError`.
  </Card>

  <Card title="Preserve idempotency" icon="repeat">
    Let the SDK mint keys, or reuse your explicit key when replaying the same
    job-creating request.
  </Card>
</CardGroup>

## Next steps

* [Job management](/sdks/typescript/job-management)
* [Async and concurrency](/sdks/typescript/async)
* [Errors and limits](/concepts/errors)
