iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Result & ?

Result<T, E> is Rust’s answer to error handling: a value is either Ok(T) or Err(E), and the compiler refuses to let you ignore the error path. The ? operator and the ? trait stack make propagation as ergonomic as exceptions, with none of the surprises.

Result + ?, error types, anyhow/thiserror

EXAMPLE
use std::fs;
use std::io;
use std::num::ParseIntError;
use std::path::Path;

// 1) The basic shape
fn parse_age(s: &str) -> Result<u32, ParseIntError> {
    s.parse::<u32>()
}

fn main1() {
    match parse_age("42") {
        Ok(n)  => println!("age = {}", n),
        Err(e) => println!("error: {}", e),
    }
}

// 2) The ? operator — early return on error
fn read_age(path: &Path) -> Result<u32, Box<dyn std::error::Error>> {
    let s = fs::read_to_string(path)?;           // io::Error -> Box<dyn Error>
    let n = s.trim().parse::<u32>()?;             // ParseIntError -> Box<dyn Error>
    Ok(n)
}

// 3) Custom error type — manual
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
    NotFound(String),
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AppError::Io(e)        => write!(f, "I/O error: {}", e),
            AppError::Parse(e)     => write!(f, "parse error: {}", e),
            AppError::NotFound(s)  => write!(f, "not found: {}", s),
        }
    }
}
impl std::error::Error for AppError {}

impl From<io::Error> for AppError       { fn from(e: io::Error)       -> Self { Self::Io(e) } }
impl From<ParseIntError> for AppError   { fn from(e: ParseIntError)  -> Self { Self::Parse(e) } }

fn load_user(path: &Path) -> Result<u32, AppError> {
    let s = fs::read_to_string(path)?;            // io::Error => AppError::Io via From
    let n = s.trim().parse()?;                     // ParseIntError => AppError::Parse via From
    Ok(n)
}

// 4) thiserror — derive everything (recommended for libraries)
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError2 {
    #[error("I/O error")]
    Io(#[from] io::Error),

    #[error("parse error")]
    Parse(#[from] ParseIntError),

    #[error("not found: {0}")]
    NotFound(String),

    #[error("invalid config at {path}: {reason}")]
    InvalidConfig { path: String, reason: String },
}

fn config_age(path: &Path) -> Result<u32, AppError2> {
    let s = fs::read_to_string(path)?;
    if s.trim().is_empty() {
        return Err(AppError2::InvalidConfig {
            path: path.display().to_string(),
            reason: "empty file".into(),
        });
    }
    Ok(s.trim().parse()?)
}

// 5) anyhow — quick error type for binaries
use anyhow::{Context, Result, anyhow, bail};

fn load(path: &Path) -> Result<u32> {
    let s = fs::read_to_string(path)
        .with_context(|| format!("reading {}", path.display()))?;
    let n: u32 = s.trim().parse()
        .with_context(|| format!("parsing {} as u32", s.trim()))?;
    if n == 0 { bail!("age cannot be zero"); }
    Ok(n)
}

fn main_anyhow() -> Result<()> {
    let n = load(Path::new("./age.txt"))?;
    println!("age = {}", n);
    Ok(())
}
// anyhow::Result<T> is the right default for application code; thiserror for libraries.

// 6) Helpful Result methods
let r: Result<i32, &str> = Ok(2);
r.is_ok();                                    // true
r.is_err();                                   // false
r.unwrap();                                    // 2 — panics on Err (DON'T ship)
r.unwrap_or(0);                                // 2 (or 0 if Err)
r.unwrap_or_else(|_| 0);
r.unwrap_or_default();                          // 2 (or default::default() if Err)
r.map(|n| n + 1);                              // Ok(3)
r.map_err(|_| "renamed");
r.and_then(|n| if n > 0 { Ok(n) } else { Err("non-positive") });
r.ok();                                         // Option<i32> → Some(2)
r.err();                                        // Option<&str> → None

// 7) Combining Results
fn fetch() -> Result<String, AppError2> {
    Ok("42".into())
}
fn validate(s: String) -> Result<u32, AppError2> {
    s.trim().parse().map_err(AppError2::Parse)
}

let age = fetch().and_then(validate)?;

// 8) Collecting an iterator of Results
let inputs = vec!["1", "2", "bad", "4"];
let results: Result<Vec<u32>, _> = inputs.iter().map(|s| s.parse::<u32>()).collect();
// First Err short-circuits; the result is the first error or Ok(Vec).

// Want all errors instead?
let (ok, err): (Vec<_>, Vec<_>) = inputs
    .iter()
    .map(|s| s.parse::<u32>())
    .partition(Result::is_ok);
let ok:  Vec<u32>           = ok.into_iter().map(|r| r.unwrap()).collect();
let err: Vec<ParseIntError> = err.into_iter().map(|r| r.err().unwrap()).collect();

// 9) From Option to Result (and back)
let some: Option<u32> = Some(5);
let r: Result<u32, &str> = some.ok_or("missing");        // Ok(5)
let r: Result<u32, &str> = some.ok_or_else(|| "missing"); // lazy
let o: Option<u32> = r.ok();

// 10) Recoverable vs unrecoverable
//   Result    — recoverable; the caller chooses
//   panic!    — unrecoverable; program (or thread) aborts
//   expect    — like unwrap but with a message; use in BINARY's main() or tests, not libraries
//   debug_assert! — debug-only invariant checks

// 11) Standard error patterns
fn run() -> Result<(), Box<dyn std::error::Error>> {
    let conf = std::env::var("DATABASE_URL")?;             // missing env -> early return
    let port: u16 = std::env::var("PORT")?.parse()?;
    println!("connecting to {} on {}", conf, port);
    Ok(())
}

// 12) ? in main
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let age = load(Path::new("./age.txt"))?;
    println!("{}", age);
    Ok(())
}

// 13) Async
use tokio::fs;
async fn read_async(path: &Path) -> Result<u32, AppError2> {
    let s = fs::read_to_string(path).await?;
    Ok(s.trim().parse()?)
}

// 14) Common bugs
//   • .unwrap() in production code — panics at runtime; use ? or unwrap_or instead
//   • Returning Result<T, String> in libraries — convenient short-term, hides error structure
//   • Custom error without From impls → can't use ? to convert; provide From for each source error
//   • Ignoring an error with let _ = doThing() — Clippy warns; consider why the error doesn't matter
//   • Mixing anyhow and thiserror in a library API — keep anyhow for binaries; libraries expose a custom error
//   • Boxing errors with no source chain — derive thiserror or implement source() to keep context
//   • Forgetting #[from] on a thiserror variant — ? requires From; without it you must .map_err

Why it matters

Use ? everywhere it makes sense, derive your library errors with thiserror, and reach for anyhow::Result<T> in binaries where you just want context-rich, dynamic errors. Reserve unwrap and expect for tests, examples, and main — production code paths should always give the caller a chance to recover.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
use std::num::ParseIntError;
fn parse(s: &str) -> Result<i32, ParseIntError> {
    let n: i32 = s.parse()?;   // ? early-returns Err
    Ok(n * 2)
}
Try it Yourself »

Exercise

Early-return on error.

let n: i32 = s.parse() ;

Test yourself

Q1. Result is used for…
Q2. The ? operator…
Q3. Convert errors automatically with ? when…

Discussion

Loading…