Vyper Error Handling: Choosing Between raise and assert for Contract Safety
A practical guide to separating expected failures from internal bugs in Vyper contracts using raise and assert, including verification steps and recovery options.
14 Feb 2026, 21:09 UTC

Problem: Opaque Reverts and Wasted Gas from Misplaced Assertions
Vyper contracts frequently fail during development because error-handling statements are swapped: assert guards user input, producing a generic Panic(0x01) that frontends cannot parse, while raise statements are omitted from internal invariants, allowing corrupted state to persist silently. The practical fix is a strict boundary: use raise for expected, caller-caused failures and assert for conditions that indicate a bug if false.
Desired Outcome
You want a Vyper contract where expected failures—insufficient balances, unauthorized access, expired deadlines—revert with a readable string that tools and users can inspect. Internal invariants, such as accounting totals that must balance after a state transition, should trigger a panic code if violated, signaling a critical logic bug rather than a user mistake.
Prerequisites
- A working Vyper compiler installation. Confirm with
vyper --versionin your terminal. - Familiarity with
@externaland@internalfunction decorators. - A local EVM test environment such as Anvil or Hardhat for deployment. Local compilation and deployment require only standard user permissions; no funded wallet or mainnet access is needed.
Procedure
Step 1: Guard Expected Failures with raise
For any condition the caller can influence, use raise with a short string. This reverts the transaction, refunds unused gas to the caller, and exposes the reason in the revert data.
@external
def transfer(_to: address, _amount: uint256):
if _amount > self.balances[msg.sender]:
raise "Insufficient balance"
self.balances[msg.sender] -= _amount
self.balances[_to] += _amount
Step 2: Protect Invariants with assert
Use assert only for conditions that should be impossible if the code is correct. If an assertion fires, it indicates a vulnerability or compiler/runtime anomaly. Vyper raises Panic(0x01) and consumes all remaining gas.
@internal
def _update_total_supply(_delta: uint256):
previous: uint256 = self.total_supply
self.total_supply = previous + _delta
assert self.total_supply == previous + _delta
Step 3: Sequence Validation, State Updates, and Invariant Checks
In a withdrawal function, validate inputs with raise, update storage before external calls, and use assert only after state transitions to verify accounting invariants.
@external
def withdraw(_amount: uint256):
if _amount > self.balances[msg.sender]:
raise "Insufficient balance"
if self.locked != 0:
raise "Lock active"
self.balances[msg.sender] -= _amount
self.locked = _amount
raw_call(msg.sender, b"", value=_amount)
self.locked = 0
assert self.locked == 0
Note: Vyper performs automatic overflow and underflow checks on standard arithmetic, so manual assert guards for those cases are redundant unless you explicitly opt into unsafe math.
Expected Checks
- Compile the contract by running
vyper contract.vyin your terminal. The compiler should report no errors or unexpected warnings for the chosen statements. - Deploy to a local chain such as Anvil. Call
withdrawwith an amount larger than the balance and confirm the trace contains the stringInsufficient balance. - In a unit test, temporarily corrupt the invariant—for example, set
self.lockedto a non-zero value before the finalassert—and confirm the call reverts withPanic(0x01)rather than a string.
Recovery Options and Limitations
| Issue | Recovery / Limitation |
|---|---|
| Revert strings increase gas cost | Shorten strings. Evaluate custom-error support in your specific compiler version; syntax and availability vary across Vyper releases. |
| Assertion fires on-chain | Treat as a critical vulnerability. Pause the contract rather than attempting automated recovery. |
Frontend cannot parse Panic(0x01) | Map panic codes in your client library, but prefer raise for all caller-correctable conditions. |
Custom errors and parameterized error syntax may differ across Vyper versions. Check the release notes or changelog for your toolchain before replacing string-based raise statements. If you are unsure whether your version supports custom errors, keep strings and verify compilation locally.
Practical Verification
After deploying locally, run a script that triggers both paths and prints the revert data. For the raise path, the ABI-decoded revert reason should match your string exactly. For the assert path, the revert data should decode to Panic(uint256) with code 0x01. If the string is missing or the panic code differs, re-check your compiler version and the exact statement syntax.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.