Error Classes
All SDK errors extend the baseMonacoCoreError class and include standardized properties for handling errors gracefully.
Base Error Properties
code: MonacoErrorCode - Standardized error codemessage: string - Human-readable error descriptionsuggestion?: string - Actionable fix suggestionretryable: boolean - Whether retrying the operation might succeedtimestamp: number - Unix timestamp when error occurredcause?: unknown - Original error that caused this error
Error Codes
EveryMonacoCoreError carries a code literal that identifies its class. Four are in use, one per exported class:
@0xmonaco/core exports exactly five error classes — MonacoCoreError, InvalidConfigError, InvalidStateError, APIError, and ContractError — and nothing else from its error module. There is no ERROR_CODES constant and no OrderError, NetworkError, InvalidAuthError, or RateLimitError to import: a network failure, an expired session, a rate limit, and a rejected order all surface as an APIError, distinguished by statusCode and the response envelope (see APIError and Order rejections). Client-side parameter validation is the one error that is not a MonacoCoreError: it is a ValidationError from @0xmonaco/types (see ValidationError).
Error Types
InvalidConfigError
Thrown when SDK is initialized or used with invalid configuration.- Missing required configuration (network, RPC URL, etc.)
- Invalid configuration values
- Wallet client without account (v0.5.6+)
InvalidStateError
Thrown when a method is called before its prerequisite is in place. Today onlysdk.getAccountAddress() throws it: Wallet client not set when the SDK was built without a walletClient, and No account available when the wallet client has no account. Calling an authenticated API method before login() is not an InvalidStateError — it is an APIError with statusCode 401 (see below).
currentState? and expectedState? — when the SDK supplies an expected state, suggestion names it. There is no field on this class; that property belongs to InvalidConfigError.
ValidationError
Thrown before any request is sent when parameters fail the SDK’s client-side schema — a non-UUIDtradingPairId, a non-positive quantity or price, a LIMIT without price, a MARKET with timeInForce, more than 100 batch items, or invalid pagination on a read. It is exported by @0xmonaco/types (not @0xmonaco/core), extends the plain Error — so it has no code, suggestion, or retryable — and is never worth retrying: fix the parameters.
APIError
Thrown when API requests fail (HTTP/REST errors).retryable classification below is safe to auto-retry for reads (getPaginatedOrders, getOrder, balances, market data) and for idempotent mutations — cancelling an order or closing a position converges on the same end state however many times it runs, so a resubmit cannot double-apply.
Retryable Status Codes (reads and idempotent mutations):
- No status code (network error) - retryable
- 429 (Rate Limit) - retryable after
retryAfterseconds - 500+ (Server Error) - retryable
- 409 (Conflict) - retryable for reads; for a single order create/replace treat it as ambiguous (see below) — except
CLIENT_ORDER_ID_CONFLICTandIDEMPOTENCY_KEY_CONFLICT, which definitively reject the current submit
- 400 (Bad Request) - fix request parameters
- 401 (Unauthorized) - re-authenticate. A
401for clock skew (Request timestamp outside allowed skew) is handled for you: the SDK reads the server’s time from each response’sDateheader, signs with the corrected time once the device clock is 5 seconds or more off, and retries a signed request once after a401that corrected the clock. A WebSocketAuthenticatehandshake rejected for skew reconnects and re-signs, up to 3 times, with whatever the clock has learned since — so it recovers once a REST response has calibrated the clock; anAUTH_FAILEDframe carries no server time, so a socket-only client never calibrated by REST still ends inonReauthenticationRequired. A missing or malformedDateheader (or a response the browser may have served from cache) leaves the offset unchanged, so the SDK falls back to the device clock. Browser clients need an API gateway that exposes theDateheader over CORS; Monaco’s does. - 403 (Forbidden) - permission issue
- 404 (Not Found) - resource doesn’t exist
idempotencyKey. Reusing the same retained key with the same normalized payload returns the original committed result for 24 hours after acceptance. The SDK generates a key when omitted, but a second call generates a new one, so supply and persist your own key before sending when recovery must cross calls or process restarts. A raw unkeyed create, a retry with a new key, or a retry after 24 hours can submit a second order. A resting LIMIT can leave duplicate exposure on the book; an order that never rests — MARKET, IOC/FOK, or a LIMIT that fills on arrival — can execute again instead.
Replace is different, and safer. replaceOrder / batchReplace name the original order id, and the matching engine revalidates that original — in memory and on the book — at sequence time. Once one replacement succeeds the original is gone, so a retry is rejected with ORDER_NOT_FOUND instead of placing a second order. Duplicate execution is therefore not this endpoint’s failure mode. It is still not safe to fire blindly: ORDER_NOT_FOUND is ambiguous on its own (the original may instead have filled or been cancelled), so reconcile to find the replacement’s new order id rather than to rule out a double.
-
Definitively rejected — safe to resubmit:
401(auth-expired) and429(rate-limited) are rejected before the order reaches the book, so it was never applied — re-authenticate or back off, then resubmit. A create rejected with the maintenanceOPERATIONS_BLOCKED503(see below) was likewise blocked, not applied. -
Ambiguous create or batch item with a retained key — replay safely: reuse the exact key and normalized payload within 24 hours. Do not change the key or payload. A different payload returns
IDEMPOTENCY_KEY_CONFLICT; after 24 hours the same key may place a new order. -
Ambiguous unkeyed/expired create or replace — reconcile first, never blind-retry: after a network error, timeout, or
5xx/409with no order in the body, the first attempt may already be live or filled. Reconcile on the order stream you subscribed to before sending; treatgetPaginatedOrders/getOrderas a weak cross-check only, since they are replica-backed and an order rejected before acceptance is never written to them at all. See Retrying order mutations safely. -
Definitively rejected key reuse:
IDEMPOTENCY_KEY_CONFLICTmeans that key was already accepted with different normalized order details. The current submit was not applied; recover the original payload instead of changing the key. -
Retryable — keyed submission storage: two structured codes arrive as HTTP
503/ gRPCUNAVAILABLEon a single create, or as a per-itemresults[].error.codein a batch. They differ in whether an order may already exist, so do not treat them alike.ORDER_SUBMISSION_CAPACITY_EXCEEDEDrefuses a new key because retained submission receipts are at capacity while responses are protected by pending durability or persistence. The refusal neither accepts nor reserves the key, so nothing was applied. Capacity frees as protected receipts finish acknowledgement and persistence and become evictable — usually seconds, not the retention window — so retry the same key with bounded backoff rather than waiting for anything to expire.ORDER_SUBMISSION_UNAVAILABLEmeans the original response for a key you already used cannot currently be retrieved, or keyed admission is busy. Do not read it as a rejection: a submission under that key may already exist and may already have executed. It never authorizes duplicate execution, so retry the same key with the same normalized payload and let the replay tell you the outcome.
-
Definitively rejected, not ambiguous (only when you set a
clientOrderId): a409whoseerror.codeisCLIENT_ORDER_ID_CONFLICTmeans another of your resting orders already holds thatclientOrderId, so the current single create/replace was rejected — it was not applied, and this is the one409on a single create/replace that is not ambiguous. If a create’s response was lost, that conflict additionally confirms the earlier attempt landed and is still resting — read the order named in the message. But its absence proves nothing, so do not resubmit in order to probe for it: the handle is claimed only while an order rests, so an attempt that filled on arrival, wasIOC/FOK, or was aMARKETorder raises no conflict, and the resubmission can execute again. Reconcile on the order stream first. Outside that retry (an accidental duplicate, or a replace colliding with an unrelated resting order) the code proves only that the current submit was rejected, not that any earlier one landed. InbatchCreate/batchReplacethe same conflict is a per-itemresults[].error.codein a200response, not a transport409. See Client Order IDs.
- During a scheduled maintenance window, mutating operations (new and replace orders, TP/SL, margin changes, collateral transfers, deposits, withdrawals, faucet mint, delegated-agent config, and reward transfers) are rejected with
503and a stableOPERATIONS_BLOCKEDerror code (gRPCUNAVAILABLEwithx-maintenance-blockedresponse metadata). Sign-in, order cancels, position closes, risk simulations, and delegated-session creation stay available. Retry once maintenance ends. POST /api/v1/positions/{positionId}/close,POST /api/v1/positions/{positionId}/tp-sl,POST /api/v1/pitpass/rewards/transfer, and the unauthenticatedGET /api/v1/orderbook/{tradingPairId}return a retryable503(instead of500) when the matching engine is momentarily unreachable or restarting; non-transient engine faults still return500.
APIError with retryable: true — they fall under the 500+ retryable rule above.
Authentication availability (a different dependency):
POST /api/v1/auth/verifyreturns a retryable503that has nothing to do with the matching engine. Three causes are contract-signature (EIP-1271) verification of an eligible address that could not be completed — the chain did not answer, did not answer within the deadline, or the server was at its concurrent-verification capacity. A fourth is unrelated to contract wallets entirely: verify reads private-beta feature-flag state first, and a read that cannot be served returns the same503for any address, before any signature work. So do not attribute a503here to the chain, and do not look for the matching engine behind it.- It is not a verdict on the signature: a rejected signature is
401, and so is a wallet contract that rejects by reverting. - This one surfaces as an
APIErrorwithretryable: trueon the REST/TypeScript path. The gRPC surface (AuthService.Verify) is not a TypeScriptAPIErrorat all — it returns the statusUNAVAILABLE, which a gRPC caller branches on directly.
sdk.buildercodes.claimBuildercodeRewards clears retryable on every failure whose outcome it cannot decide, deliberately against the rule above. That is any status at or above 500 — not only 503 — and also a failure carrying no status at all, because the response was never seen and the request may well have been served. A 4xx is a decided outcome and keeps its usual classification. The claim is admitted to the sequencer before its reply is sent and carries no idempotency key, so the outcome is unknown rather than rejected and a blind retry can claim twice; a balance read does not settle it, because an admitted claim may still be queued. Within that ambiguous set, the two tagged cases are the ones still safe to retry, because both are refused before admission: a maintenance block (error: "OPERATIONS_BLOCKED") and a sequencer overload (code: "OVERLOADED"). This says nothing about the 4xx responses, which keep the SDK’s ordinary classification — the claim’s documented 409, like any 429, is still flagged retryable. Do not narrow your own handling to 503 — a 500 or a connection reset on this call is exactly as ambiguous. See claiming rewards.
ContractError
Thrown when smart contract operations fail.- Insufficient token allowance
- Insufficient gas
- Contract execution reverted
- Invalid contract parameters
Order rejections
There is noOrderError class. A rejected order create, replace, or cancel is an APIError whose responseBody carries the REST error envelope: error (the API error type, e.g. OPERATIONS_BLOCKED), message, an optional code (the stable matching-engine code, e.g. POST_ONLY_WOULD_CROSS, ORDER_NOT_FOUND, CLIENT_ORDER_ID_CONFLICT, IDEMPOTENCY_KEY_CONFLICT, SELF_TRADE_NOT_ALLOWED), statusCode, and optional details. In batchCreate / batchReplace a per-item rejection is not thrown at all — it arrives as results[].error.code in a 200 response.
SELF_TRADE_NOT_ALLOWED fires exactly when a FOK order carrying selfTradePreventionMode: "CANCEL_BOTH" finds one of your own resting orders in its fill path: that placement is rejected and nothing fills or cancels — a 400 with code: "SELF_TRADE_NOT_ALLOWED" on a single create, a per-item results[].error.code in a 200 batch response. Every other self-cross outcome is a cancellation, not an error: the call succeeds and the affected orders terminate as CANCELLED with terminalReason: "SELF_TRADE_PREVENTION" (the WebSocket cancellation event also names the other side in stpCounterpartyOrderId), so watch order status rather than catching an exception. INVALID_SELF_TRADE_PREVENTION_MODE is the per-item batchCreate / batchReplace validation code for an unrecognized selfTradePreventionMode value; the same mistake on a single create or replace is a plain 400 whose message names the valid tokens, with no structured code. Mode semantics: Self-Trade Prevention.
Wallet shortfall vs. margin shortfall. A genuine wallet shortfall — not enough
funds to place the order — surfaces as a
400 / invalid-input error whose message
begins Insufficient balance. A margin shortfall on a MARGIN order is a
different condition and now carries the risk engine’s own wording (same 400 /
invalid-input type, only the message text differs):insufficient initial margin: required=..., available=...— the order needs more initial margin than the risk bucket can supply.insufficient free collateral: requested=..., available=...— not enough free collateral in the margin account for the requested amount.
Insufficient balance) to detect a margin
shortfall. Here the wallet is not short — the margin is — so depositing more funds
does not resolve it. For insufficient initial margin, reduce the position size or
add collateral to the risk bucket; note that lowering leverage raises the required
initial margin (notional / leverage), so it makes this rejection worse, not better.
For insufficient free collateral, free up or add collateral in the margin account,
or reduce the requested amount.Retry Logic
SDK errors include aretryable property to indicate if retrying might succeed.
Basic Retry Pattern
Advanced Retry with Rate Limiting
Retrying order mutations safely
Single create and every batch-create item support an idempotency key. The SDK sends each request once and generates a key when omitted; because another SDK call generates a new key, cross-call recovery requires an explicit key persisted before the first send. Replay the same normalized payload with that key within 24 hours to receive the original committed result. Never reuse it with changed details, and never retry an unresolved submission after the window expires. Replace has no idempotency key. For an unkeyed or expired create, or a replace, the safe pattern remains reconcile, then decide — and when reconciliation is inconclusive, do not resubmit.A
2xx does not mean the order executed. An IOC or FOK that cannot fill — or a
MARKET order whose apparent liquidity is invalidated by maker-risk revalidation after
acceptance — is accepted and then rejected by the match: the call returns successfully with
matchResult.status REJECTED, and the stream emits OrderPlaced then OrderRejected.
Read matchResult.status; do not infer the outcome from the absence of an exception.clientOrderId even on orders that never rest.
It is not replayable: a slow-client disconnect (close code 1013) gaps the stream and
there is no wire sequence number to backfill from, so a subscription opened after the
timeout cannot recover the acknowledgement you missed. Subscribe first — that is what lets
an event reach you at all. But note what this does not buy you: the fan-out is
non-persistent Core NATS, and the service can re-establish an ended upstream subscription
without closing your socket, so an event can be missed while your connection looks healthy.
Subscribing makes a positive answer possible; it never makes silence meaningful.
Rank your evidence. In descending order of strength:
- The order stream. An
OrderPlaced, fill, cancellation, orOrderRejectedcarrying yourclientOrderIdis proof the order was accepted. Silence is not evidence of the opposite, and no observable condition makes it so: the fan-out is non-persistent Core NATS, and the WebSocket service can re-establish an ended upstream subscription without closing your socket, so events can be missed while your connection still reads as healthy. Subscribe before you send — that is what lets an event reach you at all — but never convert its absence into a negative answer. - Position or balance movement. Attributable movement is positive proof something
executed, and it is independent of the order read model, so it survives replica lag.
The converse does not hold: unchanged state is inconclusive, not proof nothing
executed.
getPositionsonly consults the live view for anOPENstatus filter and otherwise reads the same replica, and concurrent fills elsewhere can net a delta to zero. Treat movement as evidence for, never absence as evidence against. getOrder/getPaginatedOrders. Weakest. Written asynchronously and served from a replica, so a missing row is not evidence the order did not land — most likely right after a timeout, since a timeout usually means the system is busy. An order rejected before acceptance is never written at all; one rejected after acceptance is persisted with statusREJECTED. A row that is present is real; an absent row is inconclusive.
sdk.ws.userOrders only
adds a local handler — the SDK sends the Subscribe frame when the socket is open and
exposes no per-subscription acknowledgement, and the constructor connects
asynchronously. A subscription registered in the same breath as the place can
therefore miss the acknowledgement without any disconnect. Establish the subscription
at startup and confirm the connection is up (sdk.ws.isConnected(), or your
onStatusChange hook). Doing so is what lets a matching event reach you — it does not
make silence meaningful. Even a long-lived subscription that has been receiving other
events cannot turn silence into a negative answer, because the upstream fan-out can be
re-established without your socket changing state.
replaceOrder with a different question in mind. A retry cannot open a
duplicate — the original is revalidated at sequence time and a second attempt returns
ORDER_NOT_FOUND — so what you are resolving is which state you are in, not whether you
doubled. A successful replace assigns a new order id and marks the original
CANCELLED with terminalReason: "REPLACED", so watch the stream for the replacement
carrying your clientOrderId. getOrder(originalOrderId) is the weaker fallback and is
asymmetric: terminalReason: "REPLACED" is positive evidence your replacement landed and
you need its new id, but an original that still reads live is inconclusive — order
persistence is asynchronous, so that row can predate a replacement the engine has already
applied. Do not treat it as proof the replace failed.
Batch create: inspect per item, clean up failed legs
batchCreate returns partial success — a failed leg does not stop the others.
When the call resolves, inspect each item: a succeeded leg carries an orderId, while
an item that did not return success carries an error and an empty orderId. Never
resubmit the whole batch. A deterministic validation or business rejection can be
corrected and resubmitted by itself, but SERVICE_UNAVAILABLE has an unknown outcome:
reconcile that leg first and retry only with its original explicit idempotencyKey.
batchCreate call instead throws (network error, timeout, or a
5xx with no body), an unknown subset of the legs may already be live. Replay each
unresolved leg with its exact explicit idempotencyKey and unchanged payload within
24 hours. If a leg has no retained key or its window expired, reconcile it like a
legacy single order: use the order stream subscribed before sending, then position or
balance movement, with getPaginatedOrders only as a weak replica-backed cross-check.
If that evidence is inconclusive, hold rather than resubmit.
Error Suggestions
EveryMonacoCoreError carries a suggestion the SDK derives from the failure — the configuration field, the HTTP status and message, or the contract revert reason. ValidationError has none; read its message or getErrors() instead.
Example: Using suggestions in UI
Complete Error Handling Example
Best Practices
- Always check error types - Use
instanceofto handle different error classes - Use retry logic for retryable errors - Check
error.retryableproperty - Respect rate limits -
error.retryAfteris in seconds; multiply by 1000 before passing it tosetTimeout(which expects milliseconds) - Add jitter to backoff - When no
retryAfteris provided, use exponential backoff with a small random jitter so concurrent clients don’t retry in lockstep - Show user-friendly messages - Use
error.suggestionfor helpful guidance - Log for debugging - Include
error.code,timestamp, andcause - Handle auth errors - Re-authenticate on 401 errors and retry
- Don’t retry non-retryable errors - Avoid wasting time on errors that won’t succeed
- Retain create idempotency keys before sending - Replay a single create or batch item only with the same explicit key and normalized payload within 24 hours. For a replace, unkeyed create, or expired key after an ambiguous outcome, reconcile first on the order WebSocket stream subscribed before sending. Only a matching event resolves it: silence is never evidence the order did not land, and replica-backed order reads are only a weak cross-check. When the evidence is inconclusive, hold rather than resubmit. Cancels and position closes are safe to retry.
See Also
- TypeScript SDK - SDK setup and configuration
- Authentication - Login and auth error handling
- Order Management - Order placement error handling

