Error handling
What this teaches: what each SDK exception means, where it is raised, and how to recover.
The hierarchy
Every SDK exception derives from Error:
Error
├── ClientError 4xx HTTP response from /graphql
├── ServerError 5xx HTTP, network failure, non-JSON body
├── GraphQLError `errors` array on the GraphQL envelope
└── TxFailed broadcastTxSync returned an `err` resultA blanket except Error is the safe outer catch; the SDK never raises anything else from public methods (except for value-error / type-error cases that surface caller bugs).
When each fires
ClientError — HTTP 4xx. The most common causes are malformed requests, expired sessions, or rate-limit responses (429). The message body is truncated to the first 500 chars of the response text.
from dango.utils.error import ClientError
try:
info.query_app({"bad": "shape"})
except ClientError as exc:
if "429" in str(exc):
# rate limited — back off and retry
...
raiseServerError — HTTP 5xx, plus every network-level failure: DNS, connection refused, timeout, SSL, non-JSON body. Wrap retries around this:
import time
from dango.utils.error import ServerError
for attempt in range(3):
try:
return info.query_status()
except ServerError:
if attempt == 2:
raise
time.sleep(2 ** attempt)GraphQLError — the HTTP layer succeeded with 2xx but the GraphQL envelope carried an errors array, or it was missing both data and errors. The message concatenates every error's message with its path:
GraphQLError: invalid pair_id 'perp/xyzusd' (path=['queryApp', 'wasm_smart'])Treat this as a logical error: the request reached the server but the query failed validation or execution.
TxFailed — broadcast_tx_sync succeeded at the HTTP/GraphQL layer but the transaction was rejected by the chain. The BroadcastTxOutcome carries an err result.
Subscription errors do not raise
WebSocket subscription errors arrive through the callback as {"_error": ...}, not as exceptions. After an error event the subscription is terminal — the manager already dropped your callback from its dispatch table. See Subscriptions for the full contract.
Caller bugs: TypeError / ValueError
The SDK raises plain TypeError / ValueError for clearly-invalid caller input. These are not subclasses of Error; they signal a bug, not a runtime failure:
dango_decimal(NaN)→ValueErrordango_decimal({"complex": "object"})→TypeErrorExchange.deposit_margin(-100)→ValueErrorExchange.deposit_margin(True)→TypeError(bool is not int)Exchange.submit_order(pair, 0, kind)→ValueError("order size must be non-zero")
A RuntimeError from SingleSigner.build_unsigned_tx / sign_tx means the signer is in an incomplete state — call query_user_index() / query_next_nonce() (or use auto_resolve) first.
Reading rate-limit responses
The server emits 429 with a JSON body describing the limit. Read it via ClientError:
from dango.utils.error import ClientError
try:
info.query_status()
except ClientError as exc:
msg = str(exc)
if "429" in msg:
# parse retry-after if needed
...The Python SDK has no built-in retry. Caller is responsible.
Next
- Rate limits & quotas — what the limits are and how to back off