Recipe · Operations

Handling wallet rejection vs network vs revert

Three failure shapes deserve three UX shapes. Users canceling is silent. Network is a retry. Contract reverts are user-visible with a decoded reason.

The three shapes#

Every SDK write goes through viem; viem normalises wallet, network, and contract errors into distinct classes you can instanceof-check.

components/CreateButton.tsx
ts
import {
  UserRejectedRequestError,
  HttpRequestError,
  ContractFunctionRevertedError,
} from "viem";

const create = useCreateVesting({ address: managerAddress });

create.mutate(args, {
  onError: (err) => {
    if (err instanceof UserRejectedRequestError) {
      // User clicked Cancel in the wallet, silent recovery, no toast.
      return;
    }
    if (err instanceof HttpRequestError) {
      // Network blip, retry the SAME mutation.
      toast.error("Network blip. Retry?");
      return;
    }
    if (err instanceof ContractFunctionRevertedError) {
      // Contract reverted, show the decoded reason inline.
      toast.error(`Contract: ${err.shortMessage}`);
      return;
    }
    // SDK-level (encryptor / RPC / setup), surface the message.
    toast.error(err.message);
  },
});

User rejected, be silent#

UserRejectedRequestErrormeans the user clicked Cancel. They KNOW the tx didn't go, a toast is noise. Reset any inline pending state and otherwise stay quiet.

Network, offer retry#

HttpRequestErroris RPC-side failure. Don't retry automatically without telling the user, the wallet UI already consumed their approval, so a silent retry could mint a duplicate tx if the first one secretly succeeded mid-failure.

Revert, show the reason#

ContractFunctionRevertedError.shortMessage carries the decoded revert reason. Pair with the product's typed error list (see /vesting/errorsetc.) to catch known classes BEFORE the viem fallback, then this branch is the "we haven't typed this revert yet" surface.

See also