11# Copyright 2026 IonQ, Inc.
22# SPDX-License-Identifier: Apache-2.0
33
4- """Structured exceptions for the IonQ API client."""
4+ """Structured exceptions for the IonQ API client.
5+
6+ All exceptions inherit from `IonQError`. The hierarchy is:
7+
8+ ```
9+ IonQError
10+ +-- APIConnectionError # network / DNS failures
11+ | +-- APITimeoutError # request timed out
12+ +-- APIError # HTTP 4xx / 5xx responses
13+ | +-- BadRequestError # 400
14+ | +-- AuthenticationError # 401
15+ | +-- PermissionDeniedError # 403
16+ | +-- NotFoundError # 404
17+ | +-- RateLimitError # 429 (includes retry_after)
18+ | +-- ServerError # 5xx
19+ ```
20+
21+ Example:
22+ ```python
23+ from ionq_core import IonQClient, RateLimitError, AuthenticationError
24+
25+ client = IonQClient()
26+ try:
27+ job = create_job.sync(client=client, body=payload)
28+ except AuthenticationError:
29+ print("Invalid API key")
30+ except RateLimitError as e:
31+ print(f"Rate limited, retry after {e.retry_after}s")
32+ ```
33+ """
534
635
736class IonQError (Exception ):
8- """Base exception for all IonQ API errors."""
37+ """Base exception for all IonQ errors.
38+
39+ Catch this to handle any error raised by the library, including connection
40+ failures, API errors, polling timeouts, and job failures.
41+ """
942
1043
1144class APIConnectionError (IonQError ):
12- """Raised when a connection to the IonQ API cannot be established."""
45+ """Raised when a connection to the IonQ API cannot be established.
46+
47+ This covers DNS resolution failures, refused connections, and other
48+ network-level errors. The original ``httpx`` exception is chained
49+ via ``__cause__``.
50+ """
1351
1452
1553class APITimeoutError (APIConnectionError ):
16- """Raised when a request to the IonQ API times out."""
54+ """Raised when a request to the IonQ API times out.
55+
56+ Inherits from `APIConnectionError` so that catching connection errors
57+ also catches timeouts.
58+ """
1759
1860
1961class APIError (IonQError ):
20- """Raised when the IonQ API returns an error response."""
62+ """Raised when the IonQ API returns an HTTP error response (4xx or 5xx).
63+
64+ Attributes:
65+ status_code: The HTTP status code.
66+ body: The parsed response body (``dict`` if JSON, ``str`` otherwise,
67+ or ``None`` if the body could not be read).
68+ message: A human-readable error message extracted from the response,
69+ or a default ``"HTTP <status>"`` string.
70+ request_id: The ``x-request-id`` header from the response, useful for
71+ contacting IonQ support about a specific request.
72+ """
2173
2274 def __init__ (
2375 self ,
@@ -35,23 +87,45 @@ def __init__(
3587
3688
3789class AuthenticationError (APIError ):
38- """Raised on 401 Unauthorized."""
90+ """Raised on ``401 Unauthorized``.
91+
92+ Typically means the API key is missing, invalid, or revoked.
93+ """
3994
4095
4196class PermissionDeniedError (APIError ):
42- """Raised on 403 Forbidden."""
97+ """Raised on ``403 Forbidden``.
98+
99+ The API key is valid but lacks permission for the requested operation.
100+ """
43101
44102
45103class NotFoundError (APIError ):
46- """Raised on 404 Not Found."""
104+ """Raised on ``404 Not Found``.
105+
106+ The requested resource (job, session, backend, etc.) does not exist.
107+ """
47108
48109
49110class BadRequestError (APIError ):
50- """Raised on 400 Bad Request."""
111+ """Raised on ``400 Bad Request``.
112+
113+ The request body or query parameters failed server-side validation.
114+ Inspect ``body`` for details.
115+ """
51116
52117
53118class RateLimitError (APIError ):
54- """Raised on 429 Too Many Requests."""
119+ """Raised on ``429 Too Many Requests``.
120+
121+ The client has exceeded the API rate limit. The ``retry_after`` attribute
122+ indicates how many seconds to wait before retrying, if the server provided
123+ a ``Retry-After`` header.
124+
125+ Attributes:
126+ retry_after: Seconds to wait before retrying, or ``None`` if the
127+ server did not include a ``Retry-After`` header.
128+ """
55129
56130 def __init__ (
57131 self ,
@@ -67,7 +141,11 @@ def __init__(
67141
68142
69143class ServerError (APIError ):
70- """Raised on 5xx server errors."""
144+ """Raised on ``5xx`` server errors.
145+
146+ These are typically transient and are automatically retried by the default
147+ transport (see `IonQClient`).
148+ """
71149
72150
73151_STATUS_TO_EXCEPTION : dict [int , type [APIError ]] = {
@@ -87,6 +165,28 @@ def raise_for_status(
87165 * ,
88166 request_id : str | None = None ,
89167) -> None :
168+ """Raise an appropriate `APIError` subclass for an HTTP error status.
169+
170+ Does nothing for status codes below 400. For 4xx codes, raises the
171+ specific subclass (e.g. `AuthenticationError` for 401). For 5xx codes
172+ or unrecognized 4xx codes, raises `ServerError` or `APIError` respectively.
173+
174+ Args:
175+ status_code: The HTTP status code.
176+ body: The parsed response body.
177+ retry_after: Value from the ``Retry-After`` header, if present.
178+ message: A human-readable error message.
179+ request_id: The ``x-request-id`` response header.
180+
181+ Raises:
182+ BadRequestError: On 400.
183+ AuthenticationError: On 401.
184+ PermissionDeniedError: On 403.
185+ NotFoundError: On 404.
186+ RateLimitError: On 429.
187+ ServerError: On 5xx.
188+ APIError: On other 4xx codes.
189+ """
90190 if status_code < 400 :
91191 return
92192 exc_cls = _STATUS_TO_EXCEPTION .get (status_code , ServerError if status_code >= 500 else APIError )
0 commit comments