On this page
Handle errors
Handle failures according to where they occur. A source-chain revert means no transfer started. A destination MessageFailed event means the transfer already exists and must be retried or restored by GUID.
Decode viem errors
Include custom errors in omniseaAbi, then walk viem's cause chain:
import {
BaseError,
ContractFunctionRevertedError,
UserRejectedRequestError,
} from "viem";
export function transferErrorMessage(error: unknown) {
if (error instanceof UserRejectedRequestError) {
return "The wallet request was rejected.";
}
if (error instanceof BaseError) {
const reverted = error.walk(
(cause) => cause instanceof ContractFunctionRevertedError,
);
if (reverted instanceof ContractFunctionRevertedError) {
const name = reverted.data?.errorName;
if (name === "InvalidFee") return "The fee changed. Refresh the quote and try again.";
if (name === "InsufficientLayerZeroGas") return "Destination gas is too low. Rebuild the route options.";
if (name === "RepresentationMustUseSendOFT") return "This token is an Omniasset. Refresh its source-token role.";
if (name === "NotRepresentation") return "This token is not a recognized Omniasset on the source chain.";
if (name === "InvalidDestination") return "Choose another supported destination.";
if (name) return `Contract reverted with ${name}.`;
}
return error.shortMessage;
}
return "The transfer could not be submitted.";
}Do not show raw RPC error objects to users. Log the full cause for diagnostics and present one actionable sentence in the interface.
Common client actions
| Failure | What the UI should do |
|---|---|
| Wallet not connected | Ask the user to connect. |
| Wrong source network | Request a wagmi chain switch before quoting or sending. |
| Approval rejected | Return to the approval-required state. |
InvalidFee | Rebuild the route and fetch a new exact quote. |
InsufficientLayerZeroGas | Re-read the gas floor and rebuild options. |
| Wrong send function | Re-read oftToOriginal and choose original or Omniasset flow. |
| User rejected send | Keep the prepared form; do not retry automatically. |
| RPC unavailable | Keep inputs and allow a bounded manual retry. |
Keep transaction stages separate
type TransferStatus =
| "editing"
| "switching-network"
| "approving"
| "quoting"
| "confirming"
| "source-confirmed"
| "delivered"
| "delivery-failed";An approval failure should not clear the form. A send failure should not be shown as a destination failure. A confirmed source send should never be automatically resubmitted because destination delivery is slow.
Handle a stale quote
The contract requires exact native value. Re-quote once after InvalidFee, then return control to the user if it changes again.
function transferErrorName(error: unknown) {
if (!(error instanceof BaseError)) return undefined;
const reverted = error.walk(
(cause) => cause instanceof ContractFunctionRevertedError,
);
return reverted instanceof ContractFunctionRevertedError
? reverted.data?.errorName
: undefined;
}
async function sendWithOneFeeRefresh(input: SendTransferInput) {
try {
return await sendTransfer(input);
} catch (error) {
if (transferErrorName(error) !== "InvalidFee") throw error;
return sendTransfer(input);
}
}Do not retry wallet rejection, insufficient balance, or invalid input automatically.
ethers v6
import { isError } from "ethers";
try {
await (await source.sendOriginal(...args, { value: fee })).wait();
} catch (error) {
if (isError(error, "ACTION_REJECTED")) {
showError("The wallet request was rejected.");
} else if (isError(error, "CALL_EXCEPTION")) {
showError(`Contract reverted${error.revert?.name ? ` with ${error.revert.name}` : ""}.`);
} else {
showError("The transfer could not be submitted.");
}
}Use Contract errors for the complete error meanings. If the source transaction confirmed and the Router later emits MessageFailed, continue with Recover a transfer.