Error Handling
Handle verification failures gracefully with clear error messages and proper validation.
This is part of the Verification Strategy for World ID Bridge integration.
Why Error Handling Matters
World ID verification can fail for various reasons: invalid proofs, network issues, or replay attempts. Without proper error handling, users get confusing failures and your application becomes unreliable.
Good error handling provides clear feedback for users, debugging information for developers, graceful recovery instead of crashes, and security protection against invalid inputs.
Basic Implementation
Define Clear Error Messages
Create organized error codes that are easy to understand:
mod Errors {
pub const PROOF_VERIFICATION_FAILED: felt252 = 'Proof verification failed';
pub const INVALID_PROOF_FORMAT: felt252 = 'Invalid proof format';
pub const EMPTY_PROOF_DATA: felt252 = 'Proof data is empty';
pub const NULLIFIER_ALREADY_USED: felt252 = 'Nullifier already used';
}Validate Inputs Early
Check for common issues before attempting verification:
fn _validate_proof_input(proof: Span<felt252>) -> bool {
assert(proof.len() > 0, Errors::EMPTY_PROOF_DATA);
assert(proof.len() >= 8, Errors::INVALID_PROOF_FORMAT);
true
}Handle Verification Results
Use pattern matching to handle different outcomes:
fn _verify_human(ref self: ContractState, proof: Span<felt252>) -> bool {
// Validate input first
self._validate_proof_input(proof);
let world_id = IGroth16VerifierBN254Dispatcher {
contract_address: self.world_id_bridge.read()
};
match world_id.verify_groth16_proof_bn254(proof) {
Option::Some(public_inputs) => {
assert(public_inputs.len() >= 4, Errors::INVALID_PROOF_FORMAT);
let nullifier = *public_inputs.at(1);
assert(nullifier != 0, 'Invalid nullifier');
assert(!self.used_nullifiers.entry(nullifier).read(), Errors::NULLIFIER_ALREADY_USED);
self.used_nullifiers.entry(nullifier).write(true);
true
},
Option::None => {
assert(false, Errors::PROOF_VERIFICATION_FAILED);
}
}
}Simple Example
Here's a basic World ID contract with error handling:
#[starknet::interface]
pub trait IMyWorldIDApp<TContractState> {
fn claim_reward(ref self: TContractState, proof: Span<felt252>) -> bool;
}
mod Errors {
pub const PROOF_VERIFICATION_FAILED: felt252 = 'Proof verification failed';
pub const INVALID_PROOF_FORMAT: felt252 = 'Invalid proof format';
pub const EMPTY_PROOF_DATA: felt252 = 'Proof data is empty';
pub const NULLIFIER_ALREADY_USED: felt252 = 'Nullifier already used';
}
#[starknet::contract]
pub mod MyWorldIDApp {
use starknet::ContractAddress;
use starknet::storage::*;
use super::{Errors, IMyWorldIDApp};
#[storage]
pub struct Storage {
world_id_bridge: ContractAddress,
used_nullifiers: Map<u256, bool>,
}
#[constructor]
fn constructor(ref self: ContractState, world_id_bridge_address: ContractAddress) {
self.world_id_bridge.write(world_id_bridge_address);
}
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn _validate_proof_input(self: @ContractState, proof: Span<felt252>) -> bool {
assert(proof.len() > 0, Errors::EMPTY_PROOF_DATA);
assert(proof.len() >= 8, Errors::INVALID_PROOF_FORMAT);
true
}
fn _verify_human(ref self: ContractState, proof: Span<felt252>) -> bool {
self._validate_proof_input(proof);
// Add your verification logic here
// This is where you'd call the World ID Bridge
true // Simplified for example
}
}
#[abi(embed_v0)]
impl MyWorldIDAppImpl of IMyWorldIDApp<ContractState> {
fn claim_reward(ref self: ContractState, proof: Span<felt252>) -> bool {
if self._verify_human(proof) {
// Execute your application logic
true
} else {
false
}
}
}
}Key Points
Validate inputs before expensive operations, use descriptive error messages with assert, check array bounds before accessing elements, and handle both success and failure cases in your verification logic.
For more advanced verification patterns, see Nullifier Tracking.