Errors and utilities

Handle errors

Every API, connection and job failure raises a NaceError. An error response raises a NaceAPIError subclass:

StatusClass
400NaceBadRequestError
401NaceAuthenticationError
402NaceInsufficientCreditError (type is insufficient_credit or payment_required)
403NacePermissionDeniedError
404, 410NaceNotFoundError
409NaceConflictError
422NaceUnprocessableEntityError
429NaceRateLimitError
529NaceOverloadedError (a subclass of NaceInternalServerError)
Other 5xxNaceInternalServerError
Any other error status, such as 408 or 413NaceAPIError

Every NaceAPIError has:

AttributeTypeDescription
statusintThe HTTP status.
messagestr | NoneThe server's message, without the status or request id.
typestr | NoneDrex's error.type, such as rate_limit_error. Branch on it.
codestr | NoneThe exact cause (error.code), sent by the document routes.
issueslist[dict]Where a request failed validation (error.issues), as {"path", "message"} entries.
detailAnyThe document service's detail (error.detail), such as detail["errors"] on a refused body.
server_retryablebool | NoneThe server's error.retryable verdict, or None when it gave none.
request_idstr | NoneThe x-request-id header, else the body's request_id. Include it in support requests.
bodyAnyThe decoded JSON body, plain text, or None.
headershttpx.HeadersThe response headers.
endpointstr | NoneThe method and URL, without credentials or query.
idempotency_keystr | NoneThe Idempotency-Key the failed request carried, or None when it carried none. Document creates always carry one: after 503 job_pending, send the same body with this key to resume the job.
ClassRaised whenExtra attributes
NaceRateLimitError429retry_after_ms: the server's requested wait, or None.
NaceAPIResponseValidationErrorA successful response whose body doesn't match the expected type. A NaceAPIError subclass with the response's own status.field_path: the first bad field, such as answers.topic.confidence.
NaceAPIConnectionErrorNo response: the connection failed or dropped. Also a ConnectionError.
NaceAPITimeoutErrorAn attempt exceeded timeout. A NaceAPIConnectionError and a TimeoutError.timeout: the setting that ran out.
NaceJobFailedErrorjobs.wait saw a document job end failed or cancelled.job: the DocumentJob as last read.
NaceJobTimeoutErrorjobs.wait ran out of time; the job keeps running.job, and timeout in seconds.

Invalid arguments the SDK checks itself raise a plain NaceError before anything is sent. A few errors aren't NaceErrors: ValueError for both transport and http_client, or for uploading bytes without a file_name; pydantic.ValidationError for an unknown field on a question or source object; and OSError when a file can't be read or written.

errors.py
from nace_sdk import NaceAPIError, NaceClient, NaceRateLimitError, Noul

with NaceClient() as client:
    try:
        client.system_one(state="Hello", questions={"greeting": Noul(instructions="Is this a greeting?")})
    except NaceRateLimitError as error:
        print("slow down", error.retry_after_ms, error.request_id)
    except NaceAPIError as error:
        print(error.status, error.type, error.message, error.issues, error.request_id)

The document routes raise the same classes as system_one. On them, error.code is the exact cause, and error.detail carries the document service's detail, such as detail["errors"] listing the fields a refused body got wrong. It's None when there is no detail, as for unsupported_file_type.

Status and codeClassWhen
402NaceInsufficientCreditErrorA create while the balance isn't positive and no card pays for usage beyond it (insufficient_credit), or while the account has an unpaid invoice (payment_required, not retryable until the invoice is paid on the Billing page).
404NaceNotFoundErrorNo such job or saved schema for this account (schema_not_found).
409 idempotency_conflictNaceConflictErrorAn Idempotency-Key reused with a different body.
409 path_conflictNaceConflictErrorAnother file already has an upload's path. Upload with on_conflict="new_version" or another path.
409 session_busy, upload_expiredNaceConflictErrorAn upload session call that doesn't fit the session's state. See Upload sessions.
404 workspace_not_found, file_not_foundNaceNotFoundErrorNo such workspace or file for this account.
409NaceConflictErrordownload of Markdown content the document service failed to prepare.
410 result_expired, job_unavailableNaceNotFoundErrorA job file or the request echo has expired, or the job was created before 3 October 2026 and its results are no longer available. An expired job itself still reads, with result set to None.
422 invalid_source, invalid_request, unsupported_file_type, invalid_schemaNaceUnprocessableEntityErrorThe body or source was refused before any job was created or billed.
429NaceRateLimitErrorOver the document routes' own per-minute limit, separate from system_one's.
503 document_billing_not_configured, job_pendingNaceInternalServerErrorDocument prices aren't configured, or the job's create isn't confirmed yet. Resume a job_pending with the error's idempotency_key.

A failed multi-part upload raises NaceError, and jobs.wait raises NaceJobFailedError and NaceJobTimeoutError from the table above (see Wait for a job). See Errors for every code and the rate limits.

from nace_sdk import NaceConflictError, NaceUnprocessableEntityError

try:
    file = client.documents.upload("invoice.pdf", path="invoices/invoice.pdf")
except NaceConflictError as error:
    if error.code == "path_conflict":
        file = client.documents.upload("invoice.pdf", path="invoices/invoice.pdf", on_conflict="new_version")
    else:
        raise
try:
    client.documents.parse(file.as_source(), page_ranges=[{"start": 0, "end": 1}])
except NaceUnprocessableEntityError as error:
    print(error.code, error.message, error.detail)

See Errors and retries for what each error means.