try/catch. The firewall and
the payer-side shield return result objects, so you branch on a discriminated
field. Knowing which layer you are in tells you which pattern to use.
Typed errors: the transfer layer
Every SDK error extendsCtrlArcZError, so one catch can recognize all of them
and separate SDK failures from unexpected ones. These are raised instead of raw
viem reverts, so end users never see contract internals.
CtrlArcZError
Base class.
catch (e) { if (e instanceof CtrlArcZError) ... } matches any SDK
error below.RiskBlockedError
sendProtected refused to move funds because the firewall returned block.
Carries the full RiskReport and a flattened reasons: string[].WrongClaimCodeError
A claim used the wrong code. The transaction mined (it must record the attempt), so
this is thrown from the receipt. Carries
attemptsRemaining and txHash.TransferLockedError
Five wrong guesses: the transfer is frozen and only the sender can
cancel.
Carries transferId and txHash.TransferUnavailableError
An action could not run given the transfer’s on-chain state. Carries a stable
reason: not_pending, expired, unknown, or not_sender.ClaimOutcomeUnknownError
The claim mined but produced neither a success nor a failure event, so its result
could not be read. Carries
txHash to investigate.When each is thrown
RiskBlockedError, fromsendProtected(andsendProtectedWithPermit) before any funds move, when the pre-send risk scan blocks. PassskipRiskCheck: trueto opt out of the gate (not recommended).WrongClaimCodeError, fromclaimwhen the code is wrong. A wrong code does not revert on-chain (the contract must decrement the attempt counter), so the SDK reads the receipt and throws.TransferLockedError, fromclaimon the attempt that exhausts the five tries, and pre-flight from the gasless claim when the transfer is alreadyLOCKED.TransferUnavailableError, fromclaim,cancel, andreclaimExpiredwhen the transfer is not in a state that allows the action (already settled, expired, unknown, or the caller is not the sender).ClaimOutcomeUnknownError, fromclaimwhen the mined receipt carries neither event, a defensive last case.
Catching them
Match specific classes first, then the base class, then rethrow the rest.RiskBlockedError. It carries the
whole report, so you can render the same explanation the pre-send UI would show.
Result objects: the firewall and the shield
The firewall and the co-signer do not throw for an expected verdict. A blocked target or a vetoed spend is a normal answer, not an exception, so they return an object you branch on.RiskReport (the firewall)
check() returns a RiskReport. It is fail-closed: it can be marked
incomplete, and an incomplete check never reports safe.
Never treat
complete: false as safe. A data source was unavailable, so a
lookalike or a bait transfer could not be ruled out. Gate on report.level === 'safe' && report.complete, not on the level alone.reasons is a RiskReason with a stable code
(LOOKALIKE_ADDRESS, ZERO_VALUE_BAIT, NEW_ADDRESS, FRESH_ADDRESS,
VERIFIED_RECIPIENT, KNOWN_COUNTERPARTY, DATA_UNAVAILABLE), a severity, a
fallback English message, and structured fields when relevant (lookalikeOf,
count, sources). See The firewall for the rules.
PrecheckResult and AuthorizeResult (the co-signer)
The co-signer answers with anapproved discriminant. precheck runs only the
firewall before any account exists; authorize validates the chain-read policy and
then co-signs.
PrecheckResultis{ approved: true }or{ approved: false, reason, riskReasons? }.AuthorizeResultis{ approved: true, signature }or{ approved: false, reason, riskReasons? }.
PrivatePayOutcome (one-call checkout)
settlePrivatePayment wraps read, authorize, and submit. It never throws for a
veto; it returns ok: false.
A veto is not an error. The co-signer withholds its signature, and a spend needs
that signature, so the payment becomes impossible rather than failing partway.
That is why a veto surfaces as
{ approved: false } / { ok: false }, not as a
thrown exception. Treat it as a normal branch and explain the reason to the user.Which layer throws, which returns
The server-side gasless claim is the exception that converts throws to a result:
/api/gasless-claim catches the transfer errors and returns
{ ok, txHash?, error? } so nothing throws across HTTP. See
Bridge USDC.
Next
- The firewall verdict in depth: The firewall.
- The co-signer and veto model: The co-signer.
- Full type reference: SDK API.