Error Handling
Error Handling in GenVM defines how Intelligent Contracts terminate, report user-generated failures, surface VM-generated failures, propagate errors from non-deterministic code, and classify failures for consensus. GenVM execution can end with unrecoverable exit codes or unhandled exceptions, produce UserError and VMError results, and let contracts catch user errors from sub-VMs when using the Equivalence Principle.
Unrecoverable Errors
Exit codes terminate execution immediately:
if invalid_condition:
exit(1)Unhandled exceptions also become unrecoverable:
raise Exception("Critical error") # Becomes exit(1)UserError
User-generated errors with UTF-8 encoded messages:
# Can be caught in current sub-vm
raise gl.vm.UserError("Invalid input")
# Immediate user error, more efficient but can't be caught
gl.advanced.user_error_immediate("Insufficient funds")VMError
VM-generated errors with predefined string codes:
# Non-zero exit codes become VMError
exit(1) # Results in VMError with specific code
# Resource limit violations also trigger VMError
# (handled automatically by the VM)Catching UserError
Handle user errors from sub-VMs:
def risky_operation():
raise gl.vm.UserError("Operation failed")
try:
result = gl.eq_principle.strict_eq(risky_operation)
except gl.vm.UserError as e:
print(f"Caught user error: {e.message}")Error Propagation
Errors flow from non-deterministic to deterministic code:
def nondet_block():
if some_condition:
raise gl.vm.UserError("INVALID_STATE")
return "success"
try:
gl.eq_principle.strict_eq(nondet_block)
except gl.vm.UserError as e:
if e.message == "INVALID_STATE":
# Handle specific error condition
passVM Result Types
GenVM produces four result types:
- Return - Successful execution with encoded result
- VMError - VM errors (exit codes, resource limits)
- UserError - User-generated errors with UTF-8 messages
- InternalError - Critical VM failures (not visible to contracts)
Error Patterns for Consensus
When using run_nondet_unsafe, errors affect how validators compare results. If the leader errors, the validator needs to decide: do I agree or disagree?
One approach is to classify errors by their nature, so validators can handle each type appropriately:
- Deterministic errors (business logic, client errors): should match exactly between leader and validator
- Transient errors (network issues, server errors): both hitting a transient failure is expected
- LLM errors (malformed output): disagreeing forces a rotation to a new leader, which is usually what you want
Example: Error Classification
def validator_fn(leaders_res: gl.vm.Result) -> bool:
if not isinstance(leaders_res, gl.vm.Return):
# Leader errored — run the same logic to see if we error too
leader_msg = leaders_res.message if hasattr(leaders_res, 'message') else ''
try:
leader_fn()
return False # Leader errored but we succeeded — disagree
except gl.UserError as e:
validator_msg = str(e)
# Both hit the same business logic error — agree
if validator_msg == leader_msg:
return True
return False
except Exception:
return False
# Leader succeeded — validate the result
validator_result = leader_fn()
return compare_results(leaders_res.calldata, validator_result)This pattern ensures that consensus failures on broken LLM output or transient network issues lead to retries rather than locking in bad state.