> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Handle Flow errors

> The errors the Flow payments functions in the JavaScript SDK throw, their codes and fields, which functions throw them, and how to recover, including when to retry.

A Flow payment can go wrong in two ways. A function call can be rejected, which throws an error and usually leaves the flow unchanged. Or the flow itself can fail later, after the payment was sent, which the flow records in `failure` instead of throwing. This page covers both.

Every error the SDK throws extends `BaseError` and has a `code`. Check the class with `instanceof`, and use `error.code` to tell cases of the same class apart. Do not match on `error.message`, because its text can change. Import every class from `@dynamic-labs-sdk/client`.

## Error classes

| Class | `code` | Extra fields | Thrown by |
| :- | :- | :- | :- |
| `FlowNotFoundError` | `flow_not_found` | None | `getFlow`, `getFlowSupportedExchanges`, `attachFlowSource` |
| `FlowSourceBlockedError` | `flow_source_blocked` | `reason`: `sanctions_blocked`, `risk_blocked` | `attachFlowSource`, `getFlowQuote` |
| `FlowExchangeSourceError` | `exchange_not_configured`, `exchange_not_supported`, `exchange_currency_unsupported`, `exchange_settlement_unsupported`, `exchange_unavailable` | `retryable` | `attachFlowSource` |
| `FlowQuoteAmountError` | `amount_invalid`, `amount_not_editable` | None | `getFlowQuote` |
| `FlowQuoteUnavailableError` | `flow_quote_unavailable` | `quoteFailures` | `getFlowQuote` |
| `FlowPrepareSigningError` | `quote_expired`, `insufficient_balance` | None | `prepareFlowSigning`, `submitFlowTransaction` |
| `FlowStateConflictError` | `flow_invalid_transition`, `flow_concurrent_update`, `flow_tx_hash_already_recorded`, or a code added later | `retryable` | `attachFlowSource`, `getFlowQuote`, `prepareFlowSigning`, `submitFlowTransaction`, `broadcastFlow`, `cancelFlow` |
| `FlowSessionTokenMissingError` | `flow_session_token_missing` | None | `getFlowQuote`, `prepareFlowSigning`, `submitFlowTransaction`, `broadcastFlow`, `cancelFlow` |
| `FlowSessionExpiredError` | `flow_session_expired` | None | `getFlowQuote`, `prepareFlowSigning`, `submitFlowTransaction`, `broadcastFlow`, `cancelFlow` |
| `UserRejectedError` | `user_rejected` | None | `submitFlowTransaction` |
| `InvalidParamError` | `invalid_param_error` | None | `submitFlowTransaction` |

The sections below describe when each one happens and what your UI should do.

### FlowNotFoundError

The flow does not exist, or it belongs to a different environment than the Dynamic client's. Retrying does not help. Check the flow ID and the client's `environmentId`, and show a "payment not found" screen. The functions that need a session token report an unknown flow as `FlowSessionExpiredError` instead.

### FlowSourceBlockedError

Screening blocked the payment source. `reason` says which check blocked it:

* `sanctions_blocked`: the wallet failed screening when it was attached, or when a `deposit` mode quote changed the amount. When attaching, nothing is saved, so the payer can attach another wallet or choose another source.
* `risk_blocked`: a quote found the flow blocked by risk screening. The payer can attach a different source on the same flow.

Tell the payer this wallet cannot be used for the payment, and do not retry the same wallet. [Screening](https://www.dynamic.xyz/docs/flow/screening.md) describes what is screened and when.

### FlowExchangeSourceError

The exchange cannot be attached as the payment source. `retryable` is `true` only for `exchange_unavailable`.

| `code` | Meaning | What to do |
| :- | :- | :- |
| `exchange_not_configured` | The exchange is not set up for your environment | Offer another way to pay |
| `exchange_not_supported` | The exchange cannot fund flows | Offer another way to pay |
| `exchange_currency_unsupported` | The exchange cannot pay in the flow's currency | Offer another way to pay |
| `exchange_settlement_unsupported` | None of the flow's settlement tokens can be paid through the exchange | Offer another way to pay |
| `exchange_unavailable` | The exchange failed to issue a purchase link | Attach the same exchange again after a pause |

List only the exchanges that [`getFlowSupportedExchanges`](https://www.dynamic.xyz/docs/javascript/reference/client/get-flow-supported-exchanges) returns to avoid most of these errors.

### FlowQuoteAmountError

The quote rejected the amount the payer chose. This applies only to `deposit` mode, where the payer can change the amount.

* `amount_invalid`: the amount is malformed, too precise, or too small. Use at most 2 decimals for `USD` and at most the token's decimals for `SOURCE_TOKEN`. Ask the payer for a valid amount.
* `amount_not_editable`: the flow's amount is fixed, because the flow is not in `deposit` mode. Stop sending `amount` for this flow.

### FlowQuoteUnavailableError

No route was found to any token the flow settles in, so the quote failed. `quoteFailures` explains why each settlement token could not be quoted, and [Quote failures](#quote-failures) lists the reasons.

### FlowPrepareSigningError

Preparing the wallet transaction failed before anything was signed or sent.

* `quote_expired`: the quote expired, which happens 60 seconds after a wallet quote. Get a new quote with [`getFlowQuote`](https://www.dynamic.xyz/docs/javascript/reference/client/get-flow-quote), then let the payer confirm again.
* `insufficient_balance`: the wallet cannot cover the transfer amount or the gas. Ask the payer to fund the wallet or pay with another token.

### FlowStateConflictError

The action conflicts with the flow's current state. `code` carries the reason, and `retryable` is `true` only for `flow_concurrent_update`.

| `code` | Meaning | What to do |
| :- | :- | :- |
| `flow_invalid_transition` | The flow's state does not allow the action, for example cancelling a flow that was already paid, or a flow that expired. | Read the flow with `getFlow` and show the screen for its state. Do not retry. |
| `flow_concurrent_update` | Another request changed the flow first. | Read the flow, and retry the action if its state still allows it. |
| `flow_tx_hash_already_recorded` | The transaction hash is already recorded, possibly on another flow. | Read the flow to see whether this flow recorded it. Do not retry. |

New codes can be added, so treat an unknown code like `flow_invalid_transition`. The React mutation hooks refetch the flow on this error, so `useGetFlow` shows the real state.

### FlowSessionTokenMissingError and FlowSessionExpiredError

Every action after attaching a source needs the flow's session token, which the SDK stores on the device when the source is attached. The two errors differ in whether a token was stored:

* `FlowSessionTokenMissingError`: no token for the flow is stored on this device. The source was attached on another device, the payer cleared storage, or no source was attached yet.
* `FlowSessionExpiredError`: a token was stored, but the server no longer accepts it. The token was revoked, expired, or replaced when a source was attached again on another device, or the flow ended. The SDK removes the stored token before it throws.

Both recover the same way. Call `getFlow`. If the flow exists and the payment has not been sent, attach the source again, which stores a new token on this device. Otherwise, show the flow's state. [Reload and another device](https://www.dynamic.xyz/docs/javascript/reference/flow-ui-guide.md#reload-and-another-device) shows the code.

### UserRejectedError

The payer declined a wallet prompt during `submitFlowTransaction`. Nothing was sent, and the SDK requested a new quote with the same token and chain before it threw, so the flow is ready for another attempt. Tell the payer the payment was not approved, and let them submit again or choose another way to pay. Do not cancel the flow.

### InvalidParamError

The wallet's chain cannot sign Flow payments. `submitFlowTransaction` supports wallets on EVM, Solana, Bitcoin, Sui, and TRON. Ask the payer to use a wallet on one of those chains.

## Quote failures

`FlowQuoteUnavailableError.quoteFailures` has one entry for each settlement token that could not be quoted, with the token's `chainId`, `chainName`, and `symbol`, and a `reason`:

| `reason` | Meaning | What to offer |
| :- | :- | :- |
| `amount_below_minimum` | The amount is below the route's minimum | A larger amount or another token |
| `amount_above_maximum` | The amount is above the route's maximum | A smaller amount |
| `insufficient_balance` | The source cannot cover the amount | Another token or a funded wallet |
| `insufficient_liquidity` | The route does not have enough liquidity | Another token or chain, or a smaller amount |
| `unsupported_chain_pair` | No route exists between the source and settlement chains | Another source chain |
| `token_not_found` | The token is not recognized | Another token |
| `token_price_unavailable` | The token cannot be priced | Another token |
| `slippage_too_tight` | The slippage tolerance is too narrow for current prices | A quote with a larger `slippage` |
| `provider_rate_limited` | The routing provider is rate limiting requests | The same quote again after a pause |
| `provider_unavailable` | The routing provider is temporarily unavailable | The same quote again after a pause |
| `provider_restricted` | The routing provider cannot serve this request | Another token or chain |
| `unknown` | Any other reason, including reasons added after your SDK version | Another token or amount |

```ts theme={"system"}
import { FlowQuoteUnavailableError, getFlowQuote } from "@dynamic-labs-sdk/client";

const RETRYABLE_REASONS = new Set(["provider_rate_limited", "provider_unavailable"]);

export const quoteOrExplain = async (flowId: string, fromChainId: string, fromTokenAddress: string) => {
  try {
    return { flow: await getFlowQuote({ flowId, fromChainId, fromTokenAddress }) };
  } catch (error) {
    if (!(error instanceof FlowQuoteUnavailableError)) throw error;

    return {
      canRetry: error.quoteFailures.some(({ reason }) => RETRYABLE_REASONS.has(reason)),
      reasons: error.quoteFailures.map(({ symbol, chainName, reason }) => `${symbol} on ${chainName}: ${reason}`),
    };
  }
};
```

## Errors without a class

Some rejections have no dedicated class. Show `error.message` for them, and let the payer change their choice:

* The flow sets `disableSwaps`, and the payer's token and chain do not match a settlement. Ask the payer to pay with a token and chain you settle in.
* The payer's token cannot be priced.
* A deposit address is not available for the chosen chain and token.
* The SDK could not get the transaction to sign during `submitFlowTransaction`. Nothing was sent; read the flow and let the payer confirm again from `quoted`.

## When to retry

Most errors leave the flow unchanged, but after the payer approves a wallet payment, an error does not prove that nothing was sent. Follow these rules for `submitFlowTransaction`:

* After `UserRejectedError`, the payer can submit again. The SDK already got a new quote.
* After `FlowPrepareSigningError` with code `quote_expired`, get a new quote, then let the payer submit again.
* After any other error, do not retry. Call `getFlow` and show the screen for its state. A flow still in `quoted` was not sent, and the payer can confirm again. A flow in `signing` or later may have been sent, so show it as processing. A second approval could send a second payment.

For other functions, retry only when the error says it can succeed: `FlowStateConflictError` with `retryable` (after reading the flow), `FlowExchangeSourceError` with `retryable`, and quote failures whose reason is `provider_rate_limited` or `provider_unavailable`.

```ts theme={"system"}
import {
  FlowPrepareSigningError,
  getFlow,
  submitFlowTransaction,
  UserRejectedError,
  type WalletAccount,
} from "@dynamic-labs-sdk/client";

export const submitOnce = async (flowId: string, walletAccount: WalletAccount) => {
  try {
    return { flow: await submitFlowTransaction({ flowId, walletAccount }) };
  } catch (error) {
    if (error instanceof UserRejectedError) return { next: "submitAgain" };
    if (error instanceof FlowPrepareSigningError && error.code === "quote_expired") return { next: "requote" };
    // The payment may already be on its way
    return { flow: await getFlow({ flowId }) };
  }
};
```

## Failures after the payment is sent

Once the payment is sent, nothing throws. If the payment or its delivery fails, the flow's `executionState` or `settlementState` becomes `failed`, and `flow.failure` describes why:

```ts theme={"system"}
import { getFlow } from "@dynamic-labs-sdk/client";

const flow = await getFlow({ flowId });

if (flow.executionState === "failed" || flow.settlementState === "failed") {
  const { code, message } = flow.failure ?? {};
  // REFUND_INITIATED means the provider is returning the funds to the payer
  showFailure({ refunded: code === "REFUND_INITIATED", message });
}
```

A failed flow cannot be retried. A new attempt needs a new flow from your server. [Failures and refunds](https://www.dynamic.xyz/docs/flow/failures-and-refunds.md) lists every failure code, what it means, and what to show the payer.

## Related

* [Build a custom payment UI](https://www.dynamic.xyz/docs/javascript/reference/flow-ui-guide.md): where each error appears in the payment UI.
* [Flow payments API errors](https://www.dynamic.xyz/docs/flow/api-errors.md): the HTTP errors behind these classes.
* [Failures and refunds](https://www.dynamic.xyz/docs/flow/failures-and-refunds.md): failure codes and refunds.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.