FABRKNT
Reth Fundamentals — Your First Steps with Alloy
Working with Alloy
Lesson 3 of 11·CONTENT12 min25 XP

Treat this page as a workbench, not a blog post. The goal is to extract a reusable mental model from the source and carry it into the rest of the Fabrknt stack.

Course
Reth Fundamentals — Your First Steps with Alloy
Lesson role
CONTENT
Sequence
3 / 11

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. .await waits 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:

TypeMeaning
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.