Recipe · Operations

Error recovery patterns by class

Each typed error class carries enough information to recover specifically. These four patterns cover ~80% of production recovery flows.

Pattern 1 · Stale fee cache → invalidate + retry#

ContractRevertError means the operator updated the fee posture mid-flight; your React Query cache is stale. Invalidate the relevant queries and tell the user to retry.

patterns/fee-recovery.ts
ts
import { ContractRevertError } from "@tokenops/sdk/fhe-vesting";

const queryClient = useQueryClient();
const claim = useClaim({ address: managerAddress });

claim.mutate(args, {
  onError(err) {
    if (err instanceof ContractRevertError) {
      // Stale fee cache. Invalidate, then user retries, fresh feeInfo will
      // resolve the correct branch.
      queryClient.invalidateQueries({
        queryKey: ["tokenops-sdk", "fhe-vesting", "managerFeeInfo"],
      });
      toast.info("Fee config changed. Refresh and retry.");
    }
  },
});

Pattern 2 · Lock not lifted → render countdown#

ClaimLockedError carries err.context.unlocksAt, Unix timestamp the cliff lifts. Show a countdown; don't spam retry.

patterns/locked.ts
ts
import { ClaimLockedError } from "@tokenops/sdk/fhe-vesting";

claim.mutate(args, {
  onError(err) {
    if (err instanceof ClaimLockedError) {
      // err.context.unlocksAt is the timestamp the lock lifts.
      const secondsLeft = err.context.unlocksAt - Math.floor(Date.now() / 1000);
      setCountdown(secondsLeft);
    }
  },
});

Pattern 3 · Signature replay → request fresh sig#

AlreadyClaimedError means the admin signature was already redeemed. Single-use by design. Operator must sign a NEW Claim with a fresh nonce, surface that requirement.

patterns/sig-replay.ts
ts
import { AlreadyClaimedError } from "@tokenops/sdk/fhe-airdrop";

claim.mutate(args, {
  onError(err) {
    if (err instanceof AlreadyClaimedError) {
      // Single-use signature already redeemed. Admin must sign a fresh
      // Claim with a NEW nonce.
      toast.error("This claim was already redeemed.");
      promptOperator(`request_new_signature`, args.recipient);
    }
  },
});

Pattern 4 · Preflight surface → render reasons inline#

PreflightFailedError (disperse) carries err.context.reasons[], structured blockers. Render each as its own inline alert; don't collapse to a generic toast.

patterns/preflight.ts
ts
if (err instanceof PreflightFailedError) {
  setBlockers(err.context.reasons); // [{code, severity, hint, value?}]
}

See also