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

QrmiError — previously, QRMI’s public API returned anyhow::Result<T>, an untyped error with no structure to match on beyond its message. QrmiError is a proper enum (built with thiserror) with a .kind() method for coarse, stable matching.

C

qrmi_get_last_error_kind() -> QrmiReturnCode — new function. QrmiReturnCode itself gained many new values alongside it (see the full list below); previously the only values that existed at all were QRMI_RETURN_CODE_SUCCESS, QRMI_RETURN_CODE_ERROR (100), and QRMI_RETURN_CODE_NULL_POINTER_ERROR (101) — every vendor-related failure, without exception, was reported as the single generic QRMI_RETURN_CODE_ERROR.

Python

qrmi.QrmiError and the full exception hierarchy under it — new. Previously QRMI raised a bare, built-in RuntimeError with no QRMI-specific exception class at all; the only way to distinguish failure causes was parsing the message string.

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 (QrmiError)

C (QrmiReturnCode)

Python (qrmi.*)

A named resource (e.g. a backend) doesn’t exist

ResourceNotFound(String)

ResourceNotFoundError (111)

ResourceNotFoundError

A named task (e.g. a job) doesn’t exist, or was already removed

TaskNotFound(String)

TaskNotFoundError

TaskNotFoundError

Credentials were missing or rejected

AuthenticationFailed(String)

AuthenticationFailedError (113)

AuthenticationFailedError

Input was rejected, whether by QRMI itself before sending anything, or by the vendor’s API after receiving the request

InvalidInput(String)

InvalidInputError (109)

InvalidInputError

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 (qrmi::ibm::IBMQuantumSystem)

Yes

IBM Qiskit Runtime Service / Quantum Compute Service (qrmi::ibm::IBMQuantumComputeService)

Yes

IQM Server (qrmi::iqm::IQMServer)

Yes

Alice & Bob Felis (qrmi::alice_bob::AliceBobFelis)

Yes

Pasqal Cloud / Local

Not yet — still reports Other for everything

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

QRMI_RETURN_CODE_SUCCESS

Success.

100

QRMI_RETURN_CODE_ERROR

Generic/uncategorized failure.

101

QRMI_RETURN_CODE_NULL_POINTER_ERROR

Predates this release (unlike everything else below). Unexpected null pointer (a QRMI-internal/FFI-usage issue, not something a vendor API returns).

102

QRMI_RETURN_CODE_ENV_VAR_NOT_SET_ERROR

A required environment variable was not set.

103

QRMI_RETURN_CODE_PARSE_ERROR

A configuration value could not be parsed.

104

QRMI_RETURN_CODE_UNSUPPORTED_RESOURCE_TYPE_ERROR

Dynamic discovery was requested for an unsupported resource type.

106

QRMI_RETURN_CODE_UNSUPPORTED_PAYLOAD_ERROR

The payload variant is not supported by this backend.

107

QRMI_RETURN_CODE_TASK_NOT_READY_ERROR

The task is not in a state that allows the requested operation.

108

QRMI_RETURN_CODE_MISSING_CONFIG_KEY_ERROR

A required key was missing from a provider’s environment variable map.

109

QRMI_RETURN_CODE_INVALID_INPUT_ERROR

Input was rejected, locally or by the vendor’s API.

111

QRMI_RETURN_CODE_RESOURCE_NOT_FOUND_ERROR

A named resource (e.g. a backend) does not exist.

112

QRMI_RETURN_CODE_TASK_NOT_FOUND_ERROR

A named task (e.g. a job) does not exist.

113

QRMI_RETURN_CODE_AUTHENTICATION_FAILED_ERROR

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

Corresponding C

code(s)

Notes

QrmiError

Error (100)

New. Base class every other exception in this table subclasses (and which itself subclasses the built-in RuntimeError — the only thing QRMI ever raised in Python before this release). Also used directly for unclassified failures.

EnvVarNotSetError

EnvVarNotSetError (102)

ConfigError

ParseError (103) or MissingConfigKeyError (108)

One Python exception covers two distinct C codes — check the message text if you need to tell them apart.

UnsupportedResourceTypeError

UnsupportedResourceTypeError (104)

UnsupportedPayloadError

UnsupportedPayloadError (106)

TaskNotReadyError

TaskNotReadyError (107)

InvalidInputError

InvalidInputError (109)

ResourceNotFoundError

ResourceNotFoundError (111)

TaskNotFoundError

TaskNotFoundError (112)

AuthenticationFailedError

AuthenticationFailedError (113)

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 → InvalidInput instead of a panic.

  • AliceBobFelis‘s job submission with invalid JSON in the payload → InvalidInput instead of a panic.

  • AliceBobFelis‘s target() when the configured backend name doesn’t match anything → ResourceNotFound instead 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:

  1. Rust — Match on the QrmiError variants (or call the new .kind() for coarser matching via QrmiErrorKind) instead of anyhow::Error‘s message text:

    match qrmi.is_accessible().await {
        Err(QrmiError::ResourceNotFound(msg)) => { /* ... */ }
        Err(QrmiError::AuthenticationFailed(msg)) => { /* ... */ }
        Err(e) => { /* ... */ }
        Ok(accessible) => { /* ... */ }
    }
    
  2. C — Call the new qrmi_get_last_error_kind() and check it against QrmiReturnCode instead of parsing qrmi_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 * to char * (see below) — if your code stores it in a const 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. Adding default: (as above) avoids that regardless of how many codes get added in the future.

  3. Python — Catch the new specific exceptions instead of, or in addition to, the base qrmi.QrmiError (a new class itself; previously QRMI raised a bare RuntimeError, with no QRMI-specific class to catch at all). Order matters here: since every new exception subclasses qrmi.QrmiError, list the more specific except clauses 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.