Learn / Exchange APIs

Error handling patterns for trading APIs

Figures on this page are as of 2026-09. Fees, limits and margin tiers change — check the venue's own docs before acting on a number.

Most error handling in trading code is written as though every failure means the same thing. It does not, and the distinction that matters is whether the exchange refused your request or whether you simply do not know what happened.

Get the classification right and the correct behaviour is obvious. Get it wrong and a network blip doubles a position.

The classification

CCXT’s hierarchy encodes it well enough to borrow regardless of what you are using:

ExchangeError     — understood, refused.     Fix the request.
OperationFailed   — outcome unknown.         Find out what happened.

Four categories in practice:

CategoryMeansResponse
PermanentThe request is wrong and always will beFail loudly. Do not retry
ConditionalWrong right nowRetry only if the condition changed
TransientInfrastructureRetry with backoff — reads only
AmbiguousOutcome unknownReconcile, then decide

The fourth exists only for state-changing calls, and it is where the money is lost.

Permanent — fail loudly

Bad credentials, missing permission, malformed request, unknown symbol.

Examples: Binance -2014 (key format), -1022 (signature); Bybit 10005 (permission denied), 10010 (IP mismatch); OKX 50113 (invalid signature).

Retrying is worse than useless — it consumes rate limit and turns a clear failure into a noisy one. These should stop the process or raise an alert. A trading integration that cannot authenticate should not be quietly looping.

Check permissions at startup, so this class surfaces on boot rather than at the first order — see Binance API key permissions.

Conditional — retry only if something changed

Insufficient funds, position limits, market closed, order not found.

These are genuinely refusals, but the condition can change. Retrying the same request immediately is pointless; retrying after the situation changes is correct.

The trap is the retry loop that treats “insufficient funds” as transient and hammers the endpoint. It will never succeed, and it will earn a rate limit.

Transient — retry with backoff, for reads

Timeouts, 5xx, rate limits, maintenance.

Binance -1003 and HTTP 429, escalating to a 418 IP ban; Bybit 10006 (account-level) and 10018 (IP-level); OKX 50011.

Exponential backoff with jitter. Respect Retry-After where given. Back off properly on 429 and you will not meet 418 — see Binance API rate limits.

Note the two Bybit rate-limit codes need different fixes: 10006 means slow down, 10018 means stop sharing an IP across accounts.

Ambiguous — the one that matters

A timeout or network error on createOrder is not a failure. It is an unknown.

The request may never have arrived. It may have arrived and filled, with the response lost on the way back. The client sees the same timeout either way.

try:
    order = exchange.create_order(...)
except ccxt.ExchangeError:
    raise                                   # refused — fix it
except ccxt.OperationFailed:
    reconcile(symbol, attempt_id, attempt_time)   # unknown — go and look

Reconcile means: query open orders, query recent fills covering the attempt window, query the position. Then decide. Three requests and a second of latency, in exchange for replacing a guess with a fact.

And do not reach for a client order ID as the safety net. Binance documents newClientOrderId as unique only among open orders, so a retry is rejected while the original rests and accepted once it has filled — the exact case you needed protection for. Full detail in retrying a failed order is not safe.

Design rules

Never auto-retry a state-changing call. Reads retry freely. Writes go through reconciliation.

Catch at the branch level, not the leaf. Catching ExchangeError and OperationFailed handles subclasses you have not met yet. Enumerating leaf classes means every new one falls through to a generic handler that probably retries.

Keep the native code visible. The unified type tells you the category; the venue code (-2015, 10003, 50113) tells you which specific problem. Log both — you will want the second when debugging.

Make ambiguous outcomes loud. They are rare and consequential, which is the worst combination for a log line nobody reads. An ambiguous order failure deserves an alert.

Fail closed. If you cannot determine state, stop trading rather than proceeding on assumptions. A halted strategy is recoverable; one acting on a wrong position is not.

The AI-assisted wrinkle

A model sees a failed tool call as a result to reason about, and the natural next move on an error is to try again. It has no concept that a particular tool is unsafe to repeat.

MCP anticipates this: idempotentHint defaults to false, which is correct for order placement. But annotations are hints the server makes about itself — the schema says they “are not guaranteed to provide a faithful description of tool behavior” — so the retry decision belongs in code you control, not in a flag you read. See how LLM tool calls go wrong on orders.

FAQ

Which trading API errors should I retry?

Reads on transient failures — timeouts, 5xx, rate limits — with exponential backoff and jitter. Never retry a state-changing call automatically. Permanent errors like bad credentials or missing permissions should fail loudly rather than loop, since retrying cannot succeed and consumes rate limit.

What should I do when an order times out?

Stop and reconcile before sending anything. Query open orders, recent fills over the attempt window, and the position. A timeout means the response did not arrive, which says nothing about whether the request did — the order may be resting, or filled with the response lost.

How do I structure error handling for an exchange API?

Classify first: permanent, conditional, transient, ambiguous. Catch at the hierarchy’s branch level so unfamiliar subclasses are handled sensibly, keep the venue’s native error code in your logs alongside the unified type, and route every ambiguous outcome on a write through reconciliation rather than retry.

Should I alert on every API error?

No — transient errors are normal and alerting on them trains you to ignore alerts. Alert on permanent failures, which mean something is misconfigured, and on ambiguous outcomes for state-changing calls, which are rare, consequential and easy to miss in a log.