Lesson 3 — Rust: Result, Option, and the ? operator
Question
Rust has tons of fallible functions. Unwrapping each one by hand would turn your code into try/catch soup. Rust's ? operator does "early-return on error, unwrap on success" in one character. That's why you see ? everywhere in Alloy code.
Principle (minimum model)
Result<T, E>. Success =Ok(T), failure =Err(E). A function return type that expresses "this can fail".Option<T>. Some =Some(T), none =None. Replaces null; no unwrap needed.?operator.result?=match result { Ok(v) => v, Err(e) => return Err(e.into()) }— sugar. Errors propagate to the caller automatically..await?combo. Ubiquitous in async functions..awaitwaits for the Future +?propagates errors → synchronous-looking async code.unwrap()vs?.unwrap()= panic on error (learning / prototype).?= propagate to caller (production code).
Worked example + steps
Rust: Result, Option, and the ? operator
Almost every line of Alloy code ends with .await? or .parse()?. Time to understand what ? actually does.
1. No exceptions
Rust has no try/catch. Errors are values returned from functions:
- A function that can fail returns
Result<T, E> - A function that may not have a value returns
Option<T>
Both are enums:
enum Result<T, E> {
Ok(T),
Err(E),
}
enum Option<T> {
Some(T),
None,
}
2. Option: present or absent
let v: Vec<i32> = vec![1, 2, 3];
let first: Option<&i32> = v.first(); // Some(&1)
let empty: Vec<i32> = vec![];
let none: Option<&i32> = empty.first();// None
match first {
Some(n) => println!("got {}", n),
None => println!("empty"),
}
3. Result: success or failure
fn parse_int(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse::<i32>()
}
match parse_int("42") {
Ok(n) => println!("got {}", n),
Err(e) => println!("oops: {}", e),
}
4. The ? operator: error propagation
? says: "if this is an error, return it from this function right now; if it's Ok, give me the inner value."
fn parse_two(a: &str, b: &str) -> Result<(i32, i32), std::num::ParseIntError> {
let x = a.parse::<i32>()?; // bail on error
let y = b.parse::<i32>()?; // bail on error
Ok((x, y))
}
Without ?, you'd write the same logic with match blocks — about three times as much code.
5. Result<(), Box<dyn Error>> and eyre::Result<()>
Common return types for main:
| Type | Meaning |
|---|---|
Result<(), Box<dyn std::error::Error>> | std-only (verbose) |
eyre::Result<()> | the eyre crate's friendly version (recommended) |
eyre gives you human-readable error chains and lets different error types compose naturally. Alloy code defaults to eyre::Result<()>.
6. unwrap() and expect()
The "ignore the error" escape hatch — fine while learning, don't ship it. They panic if the value is Err or None.
let n: i32 = "42".parse().unwrap();
let n: i32 = "42".parse().expect("not an int");
7. What this looks like in Alloy
async fn main() -> eyre::Result<()> {
let provider = ProviderBuilder::new()
.connect_http("https://eth.llamarpc.com".parse()?); // ?: parse error → return
let block = provider.get_block_number().await?; // ?: RPC error → return
Ok(())
}
Almost every line uses ?. Mental model: "keep going on success, send the error up on failure."
Summary (3 lines)
Result<T, E>success/failure +Option<T>value/none = Rust's expression of "can fail" and "may be missing". The type system enforces handling.?= early-return on error;.await?= async + error propagation. Avoids try/catch soup in one character.unwrap()= learning;?= production. Next lesson: Provider for connecting to a node.