Migrating to v0.24.0: QRMI now classifies errors#
Important
Deprecation notice (since v0.23.0)
The IBM Qiskit Runtime Service API has been renamed to the IBM Quantum Compute Service API. Resource names and environment variable prefixes have changed accordingly. Legacy names remain supported until November 21, 2026. See the migration guide for details.
Starting with v0.24.0, QRMI failures are classified by why they
happened, not just reported as a generic failure. This is the first
release where this exists at all — previously, a failed call gave you
essentially two things: a human-readable message, and a bare
success/failure signal (anyhow::Error in Rust;
qrmi_get_last_error()‘s message text plus
QrmiReturnCode::Success/Error in C; a bare, built-in
RuntimeError in Python, with no QRMI-specific exception type at
all). There was no reliable, machine-readable way to tell “the backend
doesn’t exist” apart from “the request timed out” apart from “your
credentials were rejected” without parsing message text yourself.
This release adds that: a typed QrmiError enum in Rust (with a
.kind() method), a new qrmi_get_last_error_kind() function and a
much larger QrmiReturnCode enum in C, and a full hierarchy of
specific exception types in Python. A handful of places that previously
crashed the process on bad input (a malformed API key, invalid JSON in a
job payload, …) now return a normal error through this same mechanism
instead.
This is almost entirely additive: existing code that only checks “did
this call fail” keeps working unchanged — you’ll just start seeing more
precise information available if you look for it. This guide is for code
that wants to start using that: matching on QrmiError, checking
qrmi_get_last_error_kind(), or catching a specific Python exception.
What’s new#
The classification mechanism itself#
Language |
What’s new |
|---|---|
Rust |
|
C |
|
Python |
|
New, specifically-classified conditions#
On top of the mechanism itself being new, four conditions in particular are now classified with their own variant, across all three languages:
Meaning |
Rust ( |
C ( |
Python ( |
|---|---|---|---|
A named resource (e.g. a backend) doesn’t exist |
|
|
|
A named task (e.g. a job) doesn’t exist, or was already removed |
|
|
|
Credentials were missing or rejected |
|
|
|
Input was rejected, whether by QRMI itself before sending anything, or by the vendor’s API after receiving the request |
|
|
|
All four new Python exceptions, and qrmi.QrmiError itself, subclass
the built-in RuntimeError — the same class QRMI’s failures were
always raised as before this release. So code written against the old
behavior (except RuntimeError:) keeps working completely unchanged;
it’ll still catch every one of these, it just won’t get the
finer-grained distinction unless updated to catch the new, more specific
types (or qrmi.QrmiError itself) instead.
Everything not covered above — including a handful of statuses (like
HTTP 403) that some vendor APIs use inconsistently enough that guessing
would do more harm than good — still falls back to QrmiError::Other
/ QRMI_RETURN_CODE_ERROR (100) / qrmi.QrmiError. In Python,
that’s the one case where the exception raised is qrmi.QrmiError
directly rather than one of its subclasses; catching it directly (or
RuntimeError, as before) still works as a catch-all either way.
Coverage by vendor#
Vendor / API |
Classified? |
|---|---|
IBM Quantum System
( |
Yes |
IBM Qiskit Runtime Service /
Quantum Compute Service
( |
Yes |
IQM Server
( |
Yes |
Alice & Bob Felis
( |
Yes |
Pasqal Cloud / Local |
Not yet — still reports |
Full list of error codes / exceptions#
The whole classification system is new in v0.24.0, so almost every entry
in the tables below is new too — not just the four highlighted above.
Before this release, Rust had no QrmiError at all (anyhow::Error
filled that role, with no structure to match on), C had only
QRMI_RETURN_CODE_SUCCESS, QRMI_RETURN_CODE_ERROR, and
QRMI_RETURN_CODE_NULL_POINTER_ERROR — every vendor-related failure,
without exception, reported as the single generic
QRMI_RETURN_CODE_ERROR — and Python only ever raised a bare,
built-in RuntimeError, with no QRMI-specific exception class at all.
These tables are included as a complete reference for what’s available
starting now.
C (``QrmiReturnCode``)
Value |
Constant |
Meaning |
|---|---|---|
0 |
|
Success. |
100 |
|
Generic/uncategorized failure. |
101 |
|
Predates this release (unlike everything else below). Unexpected null pointer (a QRMI-internal/FFI-usage issue, not something a vendor API returns). |
102 |
|
A required environment variable was not set. |
103 |
|
A configuration value could not be parsed. |
104 |
|
Dynamic discovery was requested for an unsupported resource type. |
106 |
|
The payload variant is not supported by this backend. |
107 |
|
The task is not in a state that allows the requested operation. |
108 |
|
A required key was missing from a provider’s environment variable map. |
109 |
|
Input was rejected, locally or by the vendor’s API. |
111 |
|
A named resource (e.g. a backend) does not exist. |
112 |
|
A named task (e.g. a job) does not exist. |
113 |
|
Credentials were missing or rejected. |
105 and 110 are intentionally absent (retired during development, before this ever shipped) — don’t rely on any code not listed here being reserved for anything in particular.
Python (``qrmi.*``)
Exception |
|
Notes |
|---|---|---|
|
|
New. Base class every
other exception in
this table subclasses
(and which itself
subclasses the
built-in
|
|
|
|
|
|
One Python exception covers two distinct C codes — check the message text if you need to tell them apart. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
There’s no Python equivalent of NullPointerError (101) — that’s a
raw-pointer FFI concern specific to the C API (and, unlike everything
else in these tables, one that predates this release) and doesn’t apply
to Python.
qrmi_get_last_error() (C)#
The return type changed from const char * to char *. This makes
explicit what was always intended: the caller owns the returned string
and must release it with qrmi_string_free(). If your code already
does qrmi_string_free((char *)qrmi_get_last_error()) (matching the
examples shipped with QRMI), no changes are needed beyond recompiling —
the cast is simply no longer necessary. This also fixes a real
double-free that could occur in this area; see the release notes for
details if you’re curious.
Panics converted to errors#
A few code paths that previously crashed the process on malformed input
now return a normal Err/non-success ReturnCode/Python exception
instead:
AliceBobFelis::new()with a malformed API key →InvalidInputinstead of a panic.AliceBobFelis‘s job submission with invalid JSON in the payload →InvalidInputinstead of a panic.AliceBobFelis‘starget()when the configured backend name doesn’t match anything →ResourceNotFoundinstead of a panic.
If your application relied on process termination in these cases (e.g. a supervisor that restarts on crash), be aware these now return normally with an error instead.
How to migrate#
Most callers don’t need to change anything. If you do want to take advantage of the new classification:
Rust — Match on the
QrmiErrorvariants (or call the new.kind()for coarser matching viaQrmiErrorKind) instead ofanyhow::Error‘s message text:match qrmi.is_accessible().await { Err(QrmiError::ResourceNotFound(msg)) => { /* ... */ } Err(QrmiError::AuthenticationFailed(msg)) => { /* ... */ } Err(e) => { /* ... */ } Ok(accessible) => { /* ... */ } }
C — Call the new
qrmi_get_last_error_kind()and check it againstQrmiReturnCodeinstead of parsingqrmi_get_last_error()‘s message text. A minimal check might just look like this:if (rc != QRMI_RETURN_CODE_SUCCESS) { QrmiReturnCode kind = qrmi_get_last_error_kind(); if (kind == QRMI_RETURN_CODE_RESOURCE_NOT_FOUND_ERROR) { /* ... */ } }
A fuller example, showing the recommended pattern end to end (retrieve the message, branch on the kind, always free the message) for a
qrmi_resource_task_start()call:char *job_id = NULL; QrmiReturnCode rc = qrmi_resource_task_start(qrmi, &payload, &job_id); if (rc != QRMI_RETURN_CODE_SUCCESS) { const char *last_error = qrmi_get_last_error(); QrmiReturnCode kind = qrmi_get_last_error_kind(); switch (kind) { case QRMI_RETURN_CODE_AUTHENTICATION_FAILED_ERROR: fprintf(stderr, "authentication failed, check your credentials: %s\n", last_error); break; case QRMI_RETURN_CODE_RESOURCE_NOT_FOUND_ERROR: fprintf(stderr, "backend not found, check the resource ID: %s\n", last_error); break; case QRMI_RETURN_CODE_INVALID_INPUT_ERROR: fprintf(stderr, "the job payload was rejected: %s\n", last_error); break; default: fprintf(stderr, "failed to start a task: %s (%d)\n", last_error, kind); break; } qrmi_string_free((char *)last_error); goto error; } fprintf(stdout, "Job ID: %s\n", job_id);
Two things worth double-checking while migrating:
``qrmi_get_last_error()``‘s return type changed from
const char *tochar *(see below) — if your code stores it in aconst char *local and casts when freeing (as the pattern above and the shipped examples do), nothing needs to change beyond recompiling; the cast is now unnecessary but harmless.If you ``switch`` on ``QrmiReturnCode`` without a ``default:`` case, a stricter compiler (
-Wswitch) may now warn about the newly added values not being handled. Addingdefault:(as above) avoids that regardless of how many codes get added in the future.
Python — Catch the new specific exceptions instead of, or in addition to, the base
qrmi.QrmiError(a new class itself; previously QRMI raised a bareRuntimeError, with no QRMI-specific class to catch at all). Order matters here: since every new exception subclassesqrmi.QrmiError, list the more specificexceptclauses before the general one, or the general one will shadow them.try: qrmi.is_accessible() except qrmi.AuthenticationFailedError as e: print(f"authentication failed, check your credentials: {e}") except qrmi.ResourceNotFoundError as e: print(f"backend not found, check the resource ID: {e}") except qrmi.TaskNotFoundError as e: print(f"job not found (already deleted?): {e}") except qrmi.InvalidInputError as e: print(f"input was rejected: {e}") except qrmi.QrmiError as e: print(f"unclassified failure: {e}")
If you only care about a couple of specific conditions and want everything else to fall through generically, that works too - just keep the specific clauses first:
try: qrmi.task_start(payload) except (qrmi.ResourceNotFoundError, qrmi.TaskNotFoundError) as e: # something QRMI was told to operate on doesn't exist ... except qrmi.QrmiError as e: # anything else, classified or not ...
No configuration, environment variable, or resource-naming changes are required; this release only changes what information is available when a call fails, not what succeeds or how to call it.