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 dictexcept QubitraError is a complete catch. Each of the five subclasses implies a
different corrective action.
The six
Section titled “The six”QubitraError
Section titled “QubitraError”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.
AuthenticationError
Section titled “AuthenticationError”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_KEYexcept AuthenticationError as error: print(error.message) # names QUBITRA_API_KEYResponse: 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.
NotFoundError
Section titled “NotFoundError”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.
InvalidRequestError
Section titled “InvalidRequestError”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 RUNNINGexcept 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.
InsufficientCreditsError
Section titled “InsufficientCreditsError”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.
ProviderError
Section titled “ProviderError”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 failureandstatus_codeisNone; - the platform answering with something the SDK cannot read as the shape it expected;
client.jobs.waitgiving 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.
Timeouts and retries
Section titled “Timeouts and retries”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 requestA 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.
Branching on all of them
Section titled “Branching on all of them”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}")Failures that are not exceptions
Section titled “Failures that are not exceptions”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.