Learn / Exchange APIs

CCXT's common errors, and what they mean

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.

CCXT’s exception hierarchy is not a flat list of error names. It is a two-branch classification, and which branch an error lands in tells you whether retrying is safe. That is the most useful thing about it and the part most often missed.

The split at the top

BaseError
├── ExchangeError      — the exchange understood you and refused
└── OperationFailed    — the outcome is unknown

ExchangeError means the request arrived, was evaluated, and was rejected. The request was wrong. Retrying it unchanged will fail again. Fix the request.

OperationFailed means something went wrong in transit or on the far side. You do not know whether the request was processed.

Everything practical follows from this. For reads the distinction barely matters. For anything that changes state, it decides your entire error-handling strategy.

The ExchangeError branch

ExchangeError
├── AuthenticationError
│   ├── PermissionDenied
│   │   └── AccountNotEnabled
│   └── AccountSuspended
├── ArgumentsRequired
├── BadRequest
│   └── BadSymbol
├── OperationRejected
│   ├── NoChange
│   │   └── MarginModeAlreadySet
│   ├── MarketClosed
│   ├── ManualInteractionNeeded
│   └── RestrictedLocation
├── InsufficientFunds
├── InvalidAddress
│   └── AddressPending
├── InvalidOrder
│   ├── OrderNotFound
│   ├── OrderNotCached
│   ├── OrderImmediatelyFillable
│   ├── OrderNotFillable
│   ├── DuplicateOrderId
│   └── ContractUnavailable
├── NotSupported
├── InvalidProxySettings
└── ExchangeClosedByUser

Notes on the ones that confuse people:

AuthenticationError vs PermissionDenied. PermissionDenied is a subclass. The first means the exchange does not accept who you are; the second means it accepts who you are and will not let you do that. On Binance both can surface from -2015, which covers bad key, wrong IP and missing permission in one code. On Bybit they are better separated: 10005 is explicitly “Permission denied, please check your API key permissions.”

RestrictedLocation is a geography refusal, not a bug. Bybit returns HTTP 403 for requests from US IPs, among other causes.

InsufficientFunds is worth catching separately. It is not a code problem and not transient; something about your sizing or your margin state is wrong.

DuplicateOrderId connects to the retry story below, and does less than you would hope — see the warning at the end.

The OperationFailed branch

OperationFailed
├── NetworkError
│   ├── DDoSProtection
│   ├── RateLimitExceeded
│   ├── ExchangeNotAvailable
│   │   └── OnMaintenance
│   ├── InvalidNonce
│   │   └── ChecksumError
│   └── RequestTimeout
├── BadResponse
│   └── NullResponse
└── CancelPending

RateLimitExceeded living under NetworkError is a deliberate and correct classification. It is a back-off-and-retry condition, not a malformed request. Venue examples: Binance -1003 TOO_MANY_REQUESTS (weight-based, escalating to an IP ban signalled by HTTP 418), Bybit 10006 for account-level and 10018 for IP-level, OKX 50011.

InvalidNonce usually means clock skew rather than anything about nonces. OKX rejects requests whose timestamp differs from server time by more than 30 seconds and recommends syncing via GET /api/v5/public/time; Binance returns -1021 when the timestamp falls outside recvWindow.

RequestTimeout is the dangerous one, for reasons in the next section.

The part that matters for orders

For a read, an OperationFailed is an inconvenience: retry, get your data.

For createOrder, it means the outcome is unknown, and unknown is not the same as failed. A timeout on an order placement could mean the request never arrived, or that it arrived and filled and the response was lost. The client cannot distinguish them.

So the rule is: never blind-retry an order after an OperationFailed. Reconcile first — query open orders, recent fills and the position — then decide.

And be careful with the standard mitigation. It is widely repeated that a client-supplied order ID makes the retry idempotent. On Binance spot it does not: newClientOrderId is documented as “a unique id among open orders”, and orders reusing an ID “can be accepted only when the previous one is filled”. That protects you while the original is resting and permits the duplicate once it has filled — the exact case you needed protection for. Treat client order IDs as reconciliation aids, not safety guarantees, and check each venue’s semantics rather than generalising.

A handler shape that works

try:
    order = exchange.create_order(...)
except ccxt.ExchangeError:
    # Understood and refused. Do not retry unchanged.
    raise
except ccxt.OperationFailed:
    # Outcome unknown. Find out before doing anything.
    reconcile(symbol, attempt_id, attempt_time)

Two properties worth preserving: catch ExchangeError and OperationFailed rather than enumerating leaf classes, so new subclasses are handled sensibly; and make reconcile the only path back to placing anything.

A caveat on the hierarchy itself

This is transcribed from CCXT’s ts/src/base/errors.ts — it is source code, not an API contract. It is stable in practice, but re-check it on a major version change rather than treating it as fixed.

FAQ

Should I retry on RateLimitExceeded?

For reads, yes, with backoff. For order placement, not without reconciling first. CCXT classifies it under NetworkError and OperationFailed, meaning the outcome is unknown — it was probably rejected at the gate, but “probably” is doing too much work when the downside is a duplicate position.

What is the difference between ExchangeError and OperationFailed?

ExchangeError means the exchange received and understood the request and refused it, so the request itself is wrong and retrying unchanged will fail again. OperationFailed means something went wrong in transit, so you do not know whether it was processed. The first tells you to fix something; the second tells you to go and find out what happened.

Why am I getting InvalidNonce when I am not using nonces?

It is usually clock skew. Exchanges reject requests whose timestamp is too far from their server time — OKX uses a 30-second window and returns 50102, Binance returns -1021 when the timestamp falls outside recvWindow. Sync your system clock, and on OKX consider syncing against GET /api/v5/public/time before placing orders.

Can I rely on DuplicateOrderId to stop double-submits?

Not as a general guarantee. It depends on the venue’s uniqueness semantics, and on Binance spot those are narrower than people assume — the ID is unique only among open orders, so it can be reused after a fill. Reconcile before retrying rather than relying on the exchange to reject the duplicate.