- Flow-level failures are expected outcomes. The on-chain flows (
passage.superstate.execute,passage.superstate.authorizeWallet,passage.tokenSale.execute,passage.ondo.prepareBuy,passage.ondo.prepareSell,passage.ondo.executeSwap,passage.centrifuge.buy,passage.centrifuge.prepareBuy,passage.centrifuge.sell,passage.centrifuge.prepareSell,passage.centrifuge.placeRequest,passage.centrifuge.claim),passage.wallets.connect, andpassage.auth.signInWithWalletreturn a tagged result rather than throwing, so you handle them with a branch, not acatch. - Thrown errors are for genuinely exceptional conditions, such as calling an authenticated method without a session.
Wallet errors
WalletError is the shared vocabulary for anything the user’s wallet does. It appears as the cause on any step that asked the wallet to sign or send.
The SDK classifies whatever your
EvmWallet throws into a WalletError inside each flow, so you never classify one yourself. WalletError is exported from @passage-cl/react/universal. An exhaustive switch over it needs a disconnected case since v0.15.1.
Disconnected wallets
When anEvmWallet method cannot act because the wallet is gone, throw WalletDisconnectedError from @passage-cl/react/universal. Its constructor takes an optional message and an optional { cause }, such as new WalletDisconnectedError("The account changed.", { cause: error }). On viem or wagmi you can let the library’s own error propagate instead. The SDK reports disconnected when any error in the cause chain is one of these:
- an object with the EIP-1193 code
4100, such as viem’sUnauthorizedProviderErroror a provider’s plainProviderRpcError - wagmi’s
ConnectorNotConnectedError, matched by its name or its message, because the SDK does not depend on wagmi - a
WalletDisconnectedError, matched by its name, so one thrown by a second copy of the SDK still counts
UserRejectedRequestError, or any object with the EIP-1193 code 4001, the result is user_rejected. Since v0.17.0 the code counts on its own, so a signer that calls the provider itself and rethrows its plain ProviderRpcError reports user_rejected rather than unknown. The SDK’s checkouts and claim dialog word a disconnected cause as “Your wallet disconnected - reconnect or choose another.” A checkout also notices on its own when you stop supplying the wallet the user confirmed: once no order is in flight, it reopens the wallet step with that message. A checkout that skipped its wallet step for preselected (every provider but Superstate, which keeps it) shows the message only when a send fails.
A transaction that mined and then reverted is not a WalletError. Most flows report it as a step of their own, such as approval-reverted, swap-reverted or the Centrifuge claim’s reverted. superstate.authorizeWallet reports none: it re-checks the allow-list on-chain and ends as not-authorized unless the wallet is allow-listed. Since v0.15.0, contract_reverted also covers calldata that the backend built and your wallet sends with broadcastRawTx (Ondo swaps, Centrifuge requests and claims, the Superstate allow-list transaction). Before, a revert at gas estimation of those transactions was unknown.
Sent but not confirmed
A swap or a request that the wallet sent but whose receipt wait then failed has its own step, with the transaction hash:swap-unconfirmed (Superstate and Ondo), broadcast-unconfirmed (Superstate wallet authorization), request-unconfirmed (Centrifuge deposits and redemptions) and unconfirmed (Centrifuge claims). The transaction may still mine, so sending it again can fill the order twice. Look up txHash before you offer a retry. The SDK’s own checkouts disable the place-order button until the user leaves the step, and the SDK’s claim dialog offers no retry.
The plain swap, broadcast and request steps mean the wallet never sent the transaction, so retrying is safe. An ERC-20 approval has no unconfirmed step: approve() sets the allowance rather than adding to it, so a failed receipt wait is reported as approval, and sending it again cannot approve twice.
Superstate swap errors
passage.superstate.execute returns SwapExecutionError, tagged by step.
The wallet is only asked to sign after all read-only checks pass, so a user never signs a transaction the swap would revert on for a reason the SDK could have caught first.
Token sale errors
passage.tokenSale.execute returns TokenSaleExecutionError.
Allowance resets
Every flow that approves an ERC-20 resets a non-zero allowance to zero first. USDT-style tokens reject a non-zero to non-zeroapprove(), so the SDK resets before re-approving. This means those users see two wallet prompts instead of one, which is worth reflecting in your loading copy. The progress phases resetting-allowance and confirming-allowance-reset tell you when it happens.
Only the token sale reports a failed reset as its own step, allowance-reset or allowance-reset-reverted. The other flows report it as approval or approval-reverted. A token sale also approves on every new participation, even when the current allowance already covers the amount.
Ondo swap errors
Placing an Ondo order is two calls, so it has two error types.passage.ondo.prepareBuy and passage.ondo.prepareSell both return OndoSwapPreparationError - every arm names a remedy rather than a direction, and an unsupported chain, a refused approval and a build that would not complete are fixed the same way whichever token was being approved.
Since v0.17.0, an exhaustive
switch over OndoSwapPreparationError needs a build-refused case. Before, a requirements_not_met refusal was build-transaction. The SDK’s Ondo checkouts word it as “Complete the requirements for this offer before placing an order.”
passage.ondo.executeSwap returns OndoSwapExecutionError, for either direction.
quote-expired is a refusal to broadcast rather than a failure of a broadcast. It would revert on-chain and cost the user gas, and it is fixed by building a fresh transaction rather than by retrying the same one.spender-mismatch was removed in v0.12.0. The spender now comes off the offer’s swapContracts, and the backend compares the built transaction’s destination to the contract it serves there, so a third comparison would check one source against itself. A wrong spender still surfaces, as insufficient-allowance.Closed markets
passage.ondo.getQuote throws an HttpError when the market is closed or paused, as it does for any refused request. The API answers that case with a 409 and the code not_tradable, and isOndoNotTradableError(error) from @passage-cl/react/universal recognizes it. Tell the user to come back when the market opens rather than offering a retry.
not-tradable (OndoLoadErrorReason). While the user is still on the step the flow opened on, both Ondo checkouts replace the flow with a “Trading is closed right now” screen until the market opens. Past that step, the amount step shows the block instead, so a closed market never takes an order dialog off the screen. The review step does not read the trading status: an order there can still be placed while its quote is valid.
Centrifuge errors
Centrifuge is
@alpha, outside the compatibility contract. Its error types can still change in a minor release.Preparing a request
passage.centrifuge.prepareBuy returns CentrifugeBuyPreparationError for a deposit, and passage.centrifuge.prepareSell returns CentrifugeSellPreparationError for a redemption. Both have the steps of OndoSwapPreparationError above, with more reasons on build-refused. unsupported-chain means the offer’s vaultContracts names no proxy on that chain. A redemption approves the vault’s share token, not the funding token, so its approval and approval-reverted steps are about the share token.
An
insufficient_allowance code is insufficient-allowance, as for Ondo. Any other failed build is build-transaction. A wallet that the API’s screening blocks has no error code yet, so it is build-transaction too, as is the 403 a wallet session gets when it builds for another wallet or chain.
Placing a request
passage.centrifuge.placeRequest broadcasts a built deposit or redemption and returns CentrifugeBuyPlacementError. passage.centrifuge.buy runs both halves of a deposit and returns CentrifugeBuyError, which is either type. passage.centrifuge.sell does the same for a redemption and returns CentrifugeSellError.
The deposit checkout
Since v0.15.1, the SDK’s deposit checkout approves and builds the request when the user confirms the amount, so the review step can show the request’s fee. ACentrifugeBuyPreparationError therefore shows on the amount step, and the review step only places. If you render your own UI over useCentrifugeBuyCheckoutViewModel, its review is a CentrifugeBuyReviewUiState, which adds two arms to the shared review state:
{ type: "loading" }while there is no request to show, such as while an expired one is rebuilt.{ type: "error", reason }when there is no request to place and building one is the user’s call. Thereasonis aCentrifugeBuyReviewErrorReason:build-failed(building the request failed) orexpired(the request passed its deadline after the step’s one automatic rebuild, or arrived already past it). Send{ type: "ON_RETRY" }to build again. The approval has already mined, so a retry asks for no signature.
Claiming
passage.centrifuge.claim returns CentrifugeClaimError. It takes the vault’s share token symbol, asks the API to build the claim (passage.centrifuge.buildClaimTransaction), and has the wallet broadcast it. No approval comes first, and no amount is sent: the claim collects everything the wallet has claimable. The progress phases are building-transaction, awaiting-wallet and confirming.
The SDK’s claim dialog offers Try again for
vault-refused once, and never for nothing-to-claim, no-proxy-on-chain or unconfirmed. Every retry builds the claim again.
v0.15.0 moved the claim to the API. The SDK used to encode the claim and send it directly.
passage.centrifuge.claim now requires symbol. The confirm step is gone: a claim that was sent but did not confirm is unconfirmed, and one that mined and reverted is reverted.Unavailable checkouts
When a checkout cannot take an order at all, the SDK’s checkout replaces the flow with a “not available” screen instead of a failed step, and its god viewmodel’s state is{ type: "error", reason }. For Ondo, Centrifuge and the token sale, the reason is a CheckoutBlockedReason<T>: unsupported-chain, unknown-option, or the provider’s own T. OndoCheckoutErrorReason adds not-tradable, CentrifugeCheckoutErrorReason adds not-orderable, and TokenSaleCheckoutErrorReason adds no-price and no-payable-asset. Superstate’s reason is a SuperstateSwapLoadErrorReason: unsupported-chain or load-failed.
unknown-option applies to Ondo and Centrifuge since v0.15.0, because an Ondo order and a Centrifuge deposit send their option’s id. Since v0.16.0, optionId is required on every checkout and the SDK never picks an option, so unknown-option has one cause: optionId names no option the offer shows. The checkout blocks at mount, before any request. An offer with no options always blocks. A Superstate swap and a Centrifuge redemption ignore optionId and never block on it. An exhaustive switch on OndoCheckoutErrorReason or CentrifugeCheckoutErrorReason needs a case for it. See Building the Checkout flow.
A wallet session can also hold a checkout back, without an error state. CheckoutContainer runs Ondo and Centrifuge checkouts through a session check first. On a wallet session, the user sees “Sign in to continue” when the option has any requirement, or “Connect your wallet” when wallets.external is not the session’s wallet. These cards have no button, so render your own next step.
Wallet authorization errors
passage.superstate.authorizeWallet returns WalletAuthorizationError. Like a swap, it splits a failed allow-list transaction into broadcast (never sent, safe to retry) and broadcast-unconfirmed (sent, carries txHash; look it up before authorizing again). See Superstate Swap for the full table and the progress phases.
Wallet connection errors
passage.wallets.connect returns { type: "error", error } with a ConnectWalletFlowError, which carries a code and a retryable flag. It returns { type: "cancelled" } when its isCancelled callback abandoned the attempt. The useConnectWallet hook surfaces the same failure as a ConnectWalletError, which adds display copy in message. Respect retryable when deciding whether to offer a retry button.
passage.wallets.connect has no disconnected code. Only a rejected signature maps to user_rejected, so a wallet that disconnected while signing the challenge reports unknown.
OAuth errors
useCompleteOAuth fails with a CompleteOAuthFailureReason, which is either complete_request_failed or one of the client-side reasons below.
Wallet sign-in errors
passage.auth.signInWithWallet and signIn from useWalletSignIn resolve with a WalletSignInResult. Only a missing walletSignIn prop rejects: see Thrown errors.
On your backend,
auth.walletChallenge throws an HttpError with a 403 when wallet sign-in is not enabled for your OAuth client, or a 422 for an invalid address or a redirectUri that is not one of the client’s. auth.completeWalletSignIn throws an HttpError with a 400 when the signature does not match an active challenge or wallet sign-in is not enabled, and WritableSessionStoreRequiredError without setSession. The SDK never retries auth.completeWalletSignIn, because a challenge is single-use.
Hook load errors
Data hooks report a failed read as anERROR state with a reason: not-authenticated or generic-error. No retry helps not-authenticated until the user signs in. Offer a retry for generic-error.
A wallet session is not a CoinList user, so useOffers, useOfferDetails, useRequirements, useOptionAddresses, useKycToken, and useCoinListTokenSaleParticipations report not-authenticated on one, and no retry helps. Read offers on your server and pass them as data, and use useOfferRequirements to read an offer’s requirements without their statuses.
The ERC-20 balance hooks, useSwapQuote, useSwapOutputToken, useOfferRequirements, the Ondo and Centrifuge hooks, and useCoinListTokenSaleParticipations use the shared LoadErrorReason. On these, not-authenticated covers both a NotAuthenticatedError and an HTTP 401, including the 401 from CoinList’s JSON-RPC proxy that on-chain reads go through, so an expired session reads the same whichever request noticed it.
useOffers, useOfferDetails, useRequirements and useOptionAddresses declare their own reason types with the same two values. On these, not-authenticated means only a NotAuthenticatedError (no CoinList user), and an HTTP 401 reports generic-error.
The Ondo hooks widen it to OndoLoadErrorReason, which adds not-tradable for a closed market.
useKycToken reports its own KycTokenErrorReason, with the same two values: not-authenticated when there is no CoinList user, or generic-error for everything else.
Thrown errors
Each error class has exactly one entry point. Import the universal ones (NotAuthenticatedError, HttpError, ValidationError, InvariantError, MathError, NotImplementedError, plus apiErrorCode and isOndoNotTradableError) from @passage-cl/react/universal. The main entry point does not re-export them. Because the package is ESM-only, every entry point shares one copy of each class, so error instanceof HttpError holds whichever entry point the failing call came from.
Which session a method needs
A wallet session is not a CoinList user, soNotAuthenticatedError depends on the method.
wallets.connect is the exception in that row: it never throws, and on a wallet session it returns the not_authenticated code. A wallet session builds only for its own wallet and chain: the API answers any other with a 403, as an HttpError.
On a PassageServer, offers.* and requirements.forOffer also take an app-level token from auth.clientCredentials(), and need no session with one. A CoinList user’s session takes precedence over that token, but a call on a wallet session sends the token. Without one, offers.* throws NotAuthenticatedError on a wallet session.
Reading an API error code
HttpError is what every API call rejects with when the backend refuses a request, so it is the only way to catch a 4xx by type. Catching by type is half of it - telling two 422s apart takes the code inside the body, which is what apiErrorCode reads. Below, insufficient_allowance is the code POST /v1/ondo/swap/buy and POST /v1/ondo/swap/sell return when the wallet has not approved the swap contract for the amount. prepareBuy and prepareSell already absorb it into their insufficient-allowance step, so you only write this when you call a builder yourself.
Next steps
Allowlist a wallet
Wallet authorization, its phases, and its error steps.
Package overview
Entry points and what each one exports.