Error handling with Result and Cargo

Returning errors instead of throwing them, the ? operator, and the Cargo workflow that builds, tests and ships a crate.

Result and the ? operator

use std::fs;

fn read_port(path: &str) -> Result<u16, String> {
    let text = fs::read_to_string(path).map_err(|e| e.to_string())?;
    let port = text.trim().parse::<u16>().map_err(|e| e.to_string())?;
    Ok(port)
}

fn main() {
    match read_port("config.txt") {
        Ok(p) => println!("port {}", p),
        Err(e) => eprintln!("failed: {}", e),
    }
}
  • Result<T, E> is returned, not thrown, so failure is visible in the signature and impossible to ignore accidentally.
  • The ? operator returns early on Err and converts the error through the From trait when the types differ.
  • unwrap() and expect() panic on failure. They are appropriate in tests and prototypes, not in code that serves requests.
  • Library crates should define a typed error enum, often with the thiserror crate; binaries can use anyhow::Result<T> and add context with .context(...).
  • A panic unwinds or aborts the thread or process. Reserve it for bugs and broken invariants, not for expected conditions such as a missing file.

Do not convert every error into a string too early. Erasing the type discards the ability to match on the cause, so retry logic and HTTP status mapping become guesswork.

Cargo: layout, builds and dependencies

cargo new hello --bin      # binary crate; --lib for a library
cd hello

cargo build                # debug build into target/debug
cargo run                  # build and execute
cargo test                 # unit tests in #[cfg(test)] modules
cargo build --release      # optimised, target/release
cargo add serde --features derive
cargo clippy               # lint pass with many useful correctness lints
cargo fmt                  # apply rustfmt
[package]
name = "hello"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
anyhow = "1"

[profile.release]
lto = true
ItemPurpose
Cargo.tomlManifest: package metadata, dependencies, profiles
Cargo.lockExact resolved versions; commit it for binaries
src/main.rsEntry point of a binary crate, containing fn main
src/lib.rsRoot of a library crate, where unit tests usually live
tests/Integration tests that use the crate as an external dependency
src/bin/Extra binaries in the same package, one file each
⚠️
Integer overflow panics in a debug build but wraps in a release build, and debug assertions disappear. Run cargo test --release before shipping so behaviour you validated in development is validated in the profile you deploy.

FAQ

When is unwrap acceptable?
In tests, in examples, and after you have proven the value cannot be absent — for instance right after inserting a key into a map. Everywhere else prefer ?, unwrap_or_else or an explicit match.
thiserror or anyhow?
thiserror for library crates that must expose typed, matchable errors. anyhow for applications where a readable chain of context is more useful than an exhaustive error enum.

Structs, enums and pattern matching Syntax and ownership

Last refreshed 2026-09-18.