Do not panic in callbacks
Handle failed cross-contract calls without aborting the cleanup or refund path.
A callback runs in its own receipt. If it panics, all state changes made by that callback are reverted, and any later transfer is never scheduled.
Do not panic when an expected cross-contract call fails. Settle the operation instead.
Panicking is only one way a callback can fail. See callbacks can fail.
The vulnerable pattern
This callback tries to remove a pending mint and refund the user after the mint fails:
private mint_callback(user, amount):
if call failed
pending_mints.remove(user)
transfer amount NEAR tokens to user
panic("mint failed") // [!code --]
The panic aborts the callback. The pending mint is restored because its removal is reverted, and the transfer line never runs.
How the issue happens
- The contract calls another contract to mint for a user, attaching NEAR tokens.
- The mint fails, so the attached NEAR tokens return to the calling contract.
mint_callbackstarts, removes the pending operation, then panics.- The entire callback receipt is reverted: the pending operation remains and no refund transfer is scheduled.
The user has neither the mint nor their NEAR tokens back. Repeating the operation can make the state even harder to settle.
The safe pattern
Handle the failure and return normally:
private mint_callback(user, amount):
pending_mints.remove(user)
if call failed
transfer amount NEAR tokens to user
else
record minted token for user
Now the cleanup persists and the refund is scheduled. If the refund needs more checks or gas than this callback can safely provide, credit a withdrawable balance instead.
General rule
Use panic for invalid input or a broken invariant before changing state. In a callback's expected failure path, record the outcome, clean up, and refund or otherwise settle the operation without panicking.
See refund NEAR tokens for the refund pattern and private callbacks for callback access control.