The three shapes#
Every SDK write goes through viem, but the SDK's revert mapper translates viem's wallet, network, and contract errors into typed SDK classes before onError fires. You instanceof-check the SDK classes, not viem's.
import {
WalletRejectedError,
NetworkError,
ContractRevertError,
} from "@tokenops/sdk";
const create = useCreateVesting({ address: managerAddress });
create.mutate(args, {
onError: (err) => {
if (err instanceof WalletRejectedError) {
// User clicked Cancel in the wallet, silent recovery, no toast.
return;
}
if (err instanceof NetworkError) {
// Network blip, retry the SAME mutation.
toast.error("Network blip. Retry?");
return;
}
if (err instanceof ContractRevertError) {
// Contract reverted, show the decoded reason inline.
toast.error(`Contract: ${err.context.revertReason ?? err.message}`);
return;
}
// SDK-level (encryptor / RPC / setup), surface the message.
toast.error(err.message);
},
});User rejected, be silent#
WalletRejectedErrormeans 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#
NetworkErroris 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#
ContractRevertError.context.revertReason carries the decoded revert reason (fall back to err.messagefor raw reverts the SDK couldn't decode). Pair with the product's typed error list (see /vesting/errorsetc.) to catch known classes BEFORE this fallback, then this branch is the "we haven't typed this revert yet" surface.