Skip to content

Error handling

Every failure the SDK raises is a QubitraError. No HTTP exception, vendor error body, or JSON decode error reaches your code.

from qubitra import QubitraError
try:
job = client.jobs.submit(backend_id="sim-statevector-26q", circuit=BELL, shots=1024)
except QubitraError as error:
print(error.message) # the platform's explanation
print(error.status_code) # the HTTP status, or None if no response was received
print(error.detail) # the structured error body, as a dict

except QubitraError is a complete catch. Each of the five subclasses implies a different corrective action.

The base class. Catch it as a backstop, and read status_code and detail when logging. detail is the platform’s structured error body; it carries a machine-readable code alongside the human-readable text, which makes it the right thing to put in a log line.

Response: log it and stop. To branch on the kind of failure, catch the subclass rather than inspecting status_code on the base class.

Missing or rejected credentials: no key, an unknown key, or a key that is not allowed to do this. It is also raised before any request when the SDK cannot find a key at all:

from qubitra import AuthenticationError, QubitraClient
try:
client = QubitraClient() # no api_key=..., no QUBITRA_API_KEY
except AuthenticationError as error:
print(error.message) # names QUBITRA_API_KEY

Response: fix configuration, not code. Check that QUBITRA_API_KEY is set and is the key you think it is. The platform does not distinguish an unknown key from a missing one, so the message carries no further detail. Retrying does not help.

The requested resource does not exist for your organization. In practice: a Backend slug that is not public, or a Job id that is not yours. Another organization’s Job id raises NotFoundError rather than a permission error; the platform does not confirm that the Job exists.

from qubitra import NotFoundError
try:
job = client.jobs.get("00000000-0000-0000-0000-000000000000")
except NotFoundError:
print("no such job for this organization")

A Backend slug that is not in client.backends.list() behaves the same way, on both client.backends.get and client.jobs.submit.

Response: re-read the catalogue and pick again, or fix the id. Nothing is charged for a submission against an unknown Backend, so a retry loop that re-reads and re-picks is safe.

The request was well-formed but refused. Two distinct situations arrive here:

  • Validation — shots outside the Backend’s window, a circuit format the Backend does not accept, a malformed field. The message names the offending field.
  • Conflict — the resource is in the wrong state for the request. Reading a result before the Job is COMPLETED, or cancelling a Job that has already finished.
from qubitra import InvalidRequestError
try:
result = client.jobs.result(job.id) # job is still RUNNING
except InvalidRequestError as error:
print(error.status_code) # 409
print(error.message) # "job ... has no result: it is RUNNING"

One case is raised without a request being sent at all: a circuit_format that is not a CircuitFormat member is refused locally.

Response: change the call. For a validation failure, read the Backend’s shots_min/shots_max/supported_formats and adjust — see choosing a backend. For a conflict, wait for the state you need (client.jobs.wait) and ask again. Retrying the identical call gets the identical answer.

The request was well-formed and your organization cannot pay for it. It is a separate type from InvalidRequestError because the fix is different: top up credits, and the same call succeeds unchanged.

from qubitra import InsufficientCreditsError
try:
job = client.jobs.submit(backend_id="sim-statevector-26q", circuit=BELL, shots=1024)
except InsufficientCreditsError:
print("out of credits — top up in the console")

Response: stop submitting and top up. Nothing is charged and no execution starts, so there is nothing to clean up — and no point retrying on a timer. A Job that is charged and then fails or is cancelled is refunded.

An upstream or transport failure that maps to nothing more specific. This is the only type worth retrying. It covers:

  • the platform answering 5xx;
  • the connection failing, timing out, or being refused — the message begins transport failure and status_code is None;
  • the platform answering with something the SDK cannot read as the shape it expected;
  • client.jobs.wait giving up before the Job reached a terminal state.
import time
from qubitra import ProviderError
for attempt in range(4):
try:
backends = client.backends.list()
break
except ProviderError as error:
if attempt == 3:
raise
print(f"transient: {error.message}")
time.sleep(2**attempt)

Response: retry with backoff, then give up and surface it. Reads are safe to retry freely. A submit has already been retried by the time a transport failure reaches you (see timeouts and retries), so a ProviderError from jobs.submit means the platform stayed unreachable. A new call to jobs.submit is a new job: before resubmitting, check client.jobs.list() for one that may already have landed.

Each request may take up to 30 seconds to connect, send and receive before it counts as a transport failure. On a slow or unreliable network, raise it when you create the client:

from qubitra import QubitraClient
client = QubitraClient(timeout=120) # seconds, applied to every request

A job submit that fails in transit (a timeout, or a connection dropped before the response arrived) is retried automatically, up to three times with a short backoff. Every submit carries a unique request key, and each retry reuses it, so the platform recognises a retry of a submit it already accepted and answers with that same job. A submit whose response was lost is never run or charged twice.

If every attempt fails, jobs.submit raises ProviderError with a message beginning transport failure. Other calls, such as reads, listings and cancellations, are sent once and raise ProviderError on their first transport failure; retry them as shown above.

Ordering matters: catch the specific types before the base.

from qubitra import (
AuthenticationError,
InsufficientCreditsError,
InvalidRequestError,
NotFoundError,
ProviderError,
QubitraError,
)
def submit(client, backend_id: str, circuit: str, shots: int):
try:
return client.jobs.submit(backend_id=backend_id, circuit=circuit, shots=shots)
except AuthenticationError:
raise SystemExit("check QUBITRA_API_KEY")
except InsufficientCreditsError:
raise SystemExit("out of credits")
except NotFoundError:
raise SystemExit(f"{backend_id} is not in the catalogue")
except InvalidRequestError as error:
raise SystemExit(f"the platform refused this: {error.message}")
except ProviderError:
return None # transient — the caller retries
except QubitraError as error:
raise SystemExit(f"unexpected: {error!r}")

Two things go wrong without raising.

A Job that failed. Submitting and polling both succeed; the Job ends FAILED. The reason is on Job.error.

from qubitra import JobStatus
job = client.jobs.wait(job.id, timeout=60, poll_interval=2)
if job.status is not JobStatus.COMPLETED:
raise SystemExit(f"job {job.id} ended {job.status}: {job.error}")

A Job that fails is not charged: the credits taken when it was submitted are returned when it settles.

A status the SDK does not know. An unrecognised status reads as JobStatus.UNKNOWN rather than raising, so a client older than the platform keeps working. UNKNOWN is not terminal, so a polling loop keeps polling — bound it with a timeout.