Errors and utilities
Handle errors
Every error the SDK throws is a NaceError, except a failure while you read a streamed body (see below). An HTTP failure throws an APIError subclass:
| Status | Class |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 402 | InsufficientCreditError (error.type is insufficient_credit or payment_required) |
| 403 | PermissionDeniedError |
| 404, 410 | NotFoundError |
| 409 | ConflictError |
| 422 | UnprocessableEntityError |
| 429 | RateLimitError |
| 5xx | InternalServerError (529 is OverloadedError, a subclass) |
| Any other, such as 413 | APIError |
Every APIError has these fields:
| Field | Type | Description |
|---|---|---|
status | number | The HTTP status. |
headers | Headers | The response headers. |
body | unknown | The parsed JSON body, the text, or undefined when empty. |
requestId | string | undefined | x-request-id, else the body's request_id. Quote it in support requests. |
type | string | undefined | error.type, the class of failure, such as rate_limit_error. |
code | string | undefined | error.code, the exact cause, sent by the document routes. |
issues | Issue[] | error.issues: each place a /v1/systemone request failed validation, as { path, message }. Empty when there are none. |
detail | unknown | error.detail: the document service's detail behind a document-route error, such as the validation problems under detail.errors. |
serverRetryable | boolean | undefined | error.retryable, the server's verdict on retrying. |
idempotencyKey | string | undefined | The Idempotency-Key the failed request carried, or undefined when it carried none. Document creates always carry one: after 503 job_pending, send the same body with this key to resume the job. |
The other classes:
| Class | Thrown when | Extra fields |
|---|---|---|
RateLimitError | 429. | retryAfterMs: the server's delay from retry-after-ms or retry-after, or undefined. |
APIConnectionError | No response arrived: DNS, TLS, a dropped connection or a cut-off body. | |
APITimeoutError | An attempt ran past its timeout. A kind of APIConnectionError. | timeoutMs: the timeout that ran out. |
APIUserAbortError | Your signal aborted a request, a wait before a retry, or a wait between polls. | |
JobFailedError | jobs.wait found the job failed or cancelled. | job: the DocumentJob, with its error. |
JobTimeoutError | jobs.wait ran out of time. The job keeps running. | job, and timeoutMs: the wait budget. |
NaceError | The SDK refused your input before sending, the client configuration is invalid, or a call ended without the result it needs: a job file with no signed link (jobs.fileLink), content still in preparation after waitTimeout (jobs.fetchFile, jobs.download), an upload job that failed or is still assembling (documents.upload, completeUploadSession), or a response of an unexpected shape. |
jobs.events, the Response from jobs.fetchFile and jobs.download read their body after the headers arrive. If the connection drops or your signal aborts while that body is read, you get the runtime's own error, a TypeError or an AbortError DOMException, rather than a NaceError.
import { APIError, NaceClient, InsufficientCreditError, noul, RateLimitError } from "nace-sdk";
const client = new NaceClient();
try {
await client.systemOne({ state: "Cancel my plan", questions: { churn: noul("Does the customer want to cancel?") } });
} catch (err) {
if (err instanceof InsufficientCreditError) {
// insufficient_credit: top up or add a card; payment_required: pay the open invoice
console.log(err.type, "- see Billing in the dashboard");
} else if (err instanceof RateLimitError) {
console.log("Retry in", err.retryAfterMs, "ms");
} else if (err instanceof APIError) {
console.log(err.status, err.type, err.issues, err.requestId);
} else {
throw err;
}
}See Errors and retries for what each error means, and the error reference for every status.
On the document routes, code names the exact cause, and detail holds the document service's detail when it refused the request, such as the validation problems under detail.errors. What each failure there throws:
| What happened | What you get |
|---|---|
| A request the document service refused, such as an unsupported file type or an unknown field | UnprocessableEntityError (422), with code (invalid_request, unsupported_file_type, invalid_source and so on), and detail when the document service sent one (undefined otherwise, as for unsupported_file_type). |
The same idempotency_key with a different body | ConflictError (409), code idempotency_conflict. |
| A create while the balance isn't positive and no card pays for usage beyond it, or while the account has an unpaid invoice | InsufficientCreditError (402), with type insufficient_credit or payment_required. payment_required isn't retryable until the invoice is paid on the Billing page. |
| No such job or saved schema for this account | NotFoundError (404), code schema_not_found for a schema. |
| An upload session call that doesn't fit the session's state | ConflictError (409), code session_busy or upload_expired. See Upload sessions. |
| No such workspace or file for this account | NotFoundError (404), code workspace_not_found or file_not_found. |
| Document prices aren't configured, or the job's create isn't confirmed yet | InternalServerError (503), code document_billing_not_configured or job_pending. Resume a job_pending with the error's idempotencyKey. |
| A different file at a taken upload path | ConflictError (409), code path_conflict, under 32 MiB; a NaceError naming the failed upload job and path_conflict from 32 MiB. |
| Full content whose export failed | ConflictError (409), from fetchFile or download. |
| An expired job file or request echo, or a job created before 3 October 2026 whose results are gone | NotFoundError (410), code result_expired or job_unavailable. An expired job itself still reads, with result: null. |
| A job that failed, was cancelled, or outlasted the wait | JobFailedError or JobTimeoutError, from jobs.wait. |
| Input the SDK checks before sending | NaceError: a string source that isn't https://, a URL without a usable file name, wait_seconds outside 0 to 60, an idempotency_key that is empty or over 200 characters, both or neither of schema and schema_id, schema_version without schema_id, a job id that isn't a UUID, a bad schema id, a limit or start_row out of range, a negative part number, or upload without a file_name. |
The document routes have their own per-minute rate limit, separate from systemOne's. A create refused with 429 starts no job: retry it with the same idempotency_key. See Errors.
import { ConflictError, UnprocessableEntityError } from "nace-sdk";
try {
await client.documents.upload(new Uint8Array([37, 80, 68, 70]), { file_name: "invoice.pdf" });
} catch (err) {
if (err instanceof ConflictError && err.code === "path_conflict") {
console.log("A different invoice.pdf is already there: pass on_conflict: \"new_version\"");
} else if (err instanceof UnprocessableEntityError) {
console.log(err.code, err.detail);
} else {
throw err;
}
}