RustThe Result Type

The Result Type in Rust

Most programs encounter operations that can fail: reading a file that might not exist, parsing a number from user input, connecting to a network that might be down. Languages that rely on exceptions handle these cases invisibly — the failure can propagate anywhere in the call stack, and callers have no idea a function can fail unless they read its documentation.

Rust takes a different approach. Functions that can fail return Result<T, E>, making the possibility of failure explicit in the type signature. Every caller is forced by the compiler to decide what to do when an error occurs. No silent exceptions, no unhandled error codes, no surprises.

What is Result?

Result<T, E> is an enum with two variants:

RUST
// How Result is defined in the standard library
enum Result<T, E> {
    Ok(T),  // operation succeeded; holds the success value of type T
    Err(E), // operation failed; holds the error value of type E
}

// Both variants are in the prelude — no import needed
fn main() {
    let success: Result<i32, String> = Ok(42);
    let failure: Result<i32, String> = Err(String::from("something went wrong"));

    println!("{:?}", success); // Ok(42)
    println!("{:?}", failure); // Err("something went wrong")
}
Note
`Result`, `Ok`, and `Err` are in the Rust prelude — you never need to import them explicitly. The type parameters T (success type) and E (error type) can be any types.
Why Result is Better Than Exceptions
  • Explicit in the type — a function returning Result advertises that it can fail; no documentation required

  • Caller is forced to decide — you cannot ignore a Result without the compiler warning you

  • Composable — Result works seamlessly with map, and_then, and ? to build pipelines

  • No hidden control flow — errors travel as values, not as invisible stack-unwinding magic

  • No overhead for the happy path — unlike exceptions, returning Ok has zero cost

Functions That Return Result

Standard library operations that interact with the outside world almost always return Result.

RUST
use std::fs;
use std::num::ParseIntError;

fn main() {
    // File I/O — Result<String, io::Error>
    let content = fs::read_to_string("config.toml");
    println!("{:?}", content); // Ok("...") or Err(os error 2)

    // Parsing — Result<i32, ParseIntError>
    let good: Result<i32, ParseIntError> = "42".parse();
    let bad:  Result<i32, ParseIntError> = "abc".parse();
    println!("{:?}", good); // Ok(42)
    println!("{:?}", bad);  // Err(invalid digit found in string)

    // Integer arithmetic that can overflow (checked versions)
    let sum: Option<i32> = i32::MAX.checked_add(1); // None (overflow)
    // Note: checked_add returns Option, but many numeric ops return Result in other contexts
}
Matching on Result

The most explicit way to handle a Result is with match. You handle both the success and failure branches, and the compiler ensures you cover both.

RUST
use std::num::ParseIntError;

fn parse_age(s: &str) -> Result<u32, ParseIntError> {
    s.trim().parse::<u32>()
}

fn main() {
    let inputs = vec!["25", "  30  ", "abc", "200"];

    for input in &inputs {
        match parse_age(input) {
            Ok(age) if age > 120 => println!("{:?} => implausibly old: {}", input, age),
            Ok(age)              => println!("{:?} => valid age: {}", input, age),
            Err(e)               => println!("{:?} => error: {}", input, e),
        }
    }
    // "25"     => valid age: 25
    // "  30  " => valid age: 30
    // "abc"    => error: invalid digit found in string
    // "200"    => implausibly old: 200
}
unwrap and expect

unwrap() extracts the Ok value or panics if the result is Err. expect("message") does the same but adds a custom panic message. Both are appropriate in tests, prototypes, and situations where you are certain an error cannot occur. Avoid them in production paths.

RUST
use std::fs;

fn main() {
    // unwrap — panics with a generic message on Err
    let n: i32 = "42".parse().unwrap();
    println!("{}", n); // 42

    // expect — panics with your message on Err; prefer this over unwrap
    let n2: i32 = "42".parse().expect("hardcoded literal must parse as i32");
    println!("{}", n2); // 42

    // In tests it is common to unwrap because a failure means the test is broken
    #[cfg(test)]
    fn test_parse() {
        let result: i32 = "100".parse().unwrap();
        assert_eq!(result, 100);
    }

    // This would panic:
    // let _bad: i32 = "abc".parse().expect("oops, 'abc' is not a number");
    // thread 'main' panicked at "oops, 'abc' is not a number: ..."
}
Warning
Never use `unwrap()` on results derived from user input, network data, or file system operations in production code. Use `expect` for known-safe unwraps during development, and prefer proper error handling (`?`, `match`, `unwrap_or`) everywhere a real error is possible.
unwrap_or, unwrap_or_else, unwrap_or_default

These methods provide safe fallback values instead of panicking on Err.

RUST
fn main() {
    // unwrap_or: return Ok value or a fixed fallback
    let port: u16 = "8080".parse().unwrap_or(80);
    println!("{}", port); // 8080

    let fallback: u16 = "bad".parse().unwrap_or(80);
    println!("{}", fallback); // 80

    // unwrap_or_else: lazy — closure only called on Err
    let value: i32 = "abc".parse().unwrap_or_else(|e| {
        eprintln!("parse failed: {}", e);
        -1
    });
    println!("{}", value); // -1

    // unwrap_or_default: use the type's Default implementation
    let zero: i32 = "oops".parse().unwrap_or_default();
    println!("{}", zero); // 0  (i32::default() is 0)

    let empty: String = "123".parse::<bool>().map(|_| String::new()).unwrap_or_default();
    println!("{:?}", empty); // ""
}
is_ok and is_err

When you only need a boolean answer about whether an operation succeeded, without extracting the value, use is_ok() and is_err().

RUST
fn main() {
    let good: Result<i32, &str> = Ok(10);
    let bad:  Result<i32, &str> = Err("failed");

    println!("{}", good.is_ok());  // true
    println!("{}", good.is_err()); // false
    println!("{}", bad.is_ok());   // false
    println!("{}", bad.is_err());  // true

    // Useful for filtering
    let attempts: Vec<Result<i32, _>> = vec![
        "1".parse(), "two".parse(), "3".parse(), "four".parse(),
    ];

    let success_count = attempts.iter().filter(|r| r.is_ok()).count();
    println!("successful parses: {}", success_count); // successful parses: 2
}
Transforming Result with map and map_err

map transforms the Ok value without affecting Err. map_err transforms the Err value without affecting Ok. Both leave the other variant untouched.

RUST
use std::num::ParseIntError;

fn main() {
    // map: transform the Ok value
    let doubled: Result<i32, ParseIntError> = "21".parse::<i32>().map(|n| n * 2);
    println!("{:?}", doubled); // Ok(42)

    let err_passthrough: Result<i32, ParseIntError> = "oops".parse::<i32>().map(|n| n * 2);
    println!("{:?}", err_passthrough); // Err(invalid digit found in string)

    // map_err: convert the error type (common when unifying error types)
    #[derive(Debug)]
    struct AppError(String);

    let converted: Result<i32, AppError> = "abc"
        .parse::<i32>()
        .map_err(|e| AppError(format!("parse failed: {}", e)));
    println!("{:?}", converted); // Err(AppError("parse failed: invalid digit found in string"))

    // Chain map and map_err together
    let processed: Result<String, AppError> = "   99  "
        .trim()
        .parse::<i32>()
        .map(|n| format!("value is {}", n))
        .map_err(|e| AppError(e.to_string()));
    println!("{:?}", processed); // Ok("value is 99")
}
and_then — Chaining Fallible Operations

and_then passes the Ok value to a closure that returns another Result. If the original result is Err, it short-circuits and returns the Err unchanged. Use it to chain multiple operations that each might fail.

RUST
use std::num::ParseIntError;

fn parse_positive(s: &str) -> Result<i32, String> {
    s.trim()
     .parse::<i32>()
     .map_err(|e| e.to_string())
     .and_then(|n| {
         if n > 0 {
             Ok(n)
         } else {
             Err(format!("{} is not positive", n))
         }
     })
}

fn compute(s: &str) -> Result<i32, String> {
    parse_positive(s)
        .and_then(|n| {
            if n < 1000 {
                Ok(n * n)
            } else {
                Err(String::from("number too large to square safely"))
            }
        })
}

fn main() {
    println!("{:?}", compute("5"));    // Ok(25)
    println!("{:?}", compute("abc"));  // Err("invalid digit found in string")
    println!("{:?}", compute("-3"));   // Err("-3 is not positive")
    println!("{:?}", compute("2000")); // Err("number too large to square safely")
}
The ? Operator — Propagating Errors

The ? operator is the most important ergonomics feature for error handling in Rust. Placed after a Result expression, it does the following:

  • If the result is Ok(value), it unwraps to value and execution continues.
  • If the result is Err(e), it immediately returns Err(e) from the enclosing function (converting the error type via From if needed).

The function must return Result (or Option) for ? to work.

RUST
use std::fs;
use std::io;
use std::num::ParseIntError;

// Without ?: every step requires a match
fn read_number_verbose(path: &str) -> Result<i32, String> {
    let content = match fs::read_to_string(path) {
        Ok(s)  => s,
        Err(e) => return Err(e.to_string()),
    };
    let trimmed = content.trim();
    let number = match trimmed.parse::<i32>() {
        Ok(n)  => n,
        Err(e) => return Err(e.to_string()),
    };
    Ok(number * 2)
}

// With ?: clean and flat — each ? handles the error propagation
fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?; // propagate io::Error on failure
    let number  = content.trim().parse::<i32>()?; // propagate ParseIntError on failure
    Ok(number * 2)
}

fn main() {
    match read_number("number.txt") {
        Ok(n)  => println!("doubled: {}", n),
        Err(e) => println!("error: {}", e),
    }
}

RUST
// ? also works with multiple error types when they implement From
use std::fmt;
use std::num::ParseIntError;
use std::io;

#[derive(Debug)]
enum MyError {
    Io(io::Error),
    Parse(ParseIntError),
}

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

fn load_setting(path: &str) -> Result<i32, MyError> {
    let raw    = std::fs::read_to_string(path)?; // io::Error -> MyError::Io via From
    let parsed = raw.trim().parse::<i32>()?;     // ParseIntError -> MyError::Parse via From
    Ok(parsed)
}
Tip
Use `Box<dyn std::error::Error>` as the error type when you want maximum flexibility in a prototype or main function. Use a concrete custom error enum for library code where callers need to distinguish between error kinds.
Writing Functions That Return Result

The standard pattern for a fallible function is to declare its return type as Result<SuccessType, ErrorType> and use ? inside the body to propagate errors.

RUST
use std::fs::File;
use std::io::{self, BufRead, BufReader};

// A function that reads lines from a file and returns them as a Vec
fn read_lines(path: &str) -> Result<Vec<String>, io::Error> {
    let file   = File::open(path)?;           // Err propagated if file not found
    let reader = BufReader::new(file);
    let mut lines = Vec::new();

    for line in reader.lines() {
        lines.push(line?);                    // Err propagated if read fails
    }

    Ok(lines)
}

// main can also return Result — useful for scripts
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let lines = read_lines("input.txt")?;
    for (i, line) in lines.iter().enumerate() {
        println!("{}: {}", i + 1, line);
    }
    Ok(())
}
Collecting Results from Iterators

When mapping a fallible operation over an iterator, you often want to either collect all successes or fail fast on the first error. Rust's collect supports this with a Result<Vec<T>, E> target type.

RUST
fn main() {
    // Fail fast: if any parse fails, the whole collection is an Err
    let strings = vec!["1", "2", "3", "4"];
    let numbers: Result<Vec<i32>, _> = strings.iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("{:?}", numbers); // Ok([1, 2, 3, 4])

    let mixed = vec!["1", "oops", "3"];
    let numbers2: Result<Vec<i32>, _> = mixed.iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("{:?}", numbers2); // Err(invalid digit found in string)

    // Collect successes only, ignore errors (partition approach)
    let inputs = vec!["10", "bad", "30", "?", "50"];
    let (successes, failures): (Vec<_>, Vec<_>) = inputs.iter()
        .map(|s| s.parse::<i32>().map_err(|e| (*s, e)))
        .partition(Result::is_ok);

    let values: Vec<i32>    = successes.into_iter().map(|r| r.unwrap()).collect();
    let errors:  Vec<(&str, _)> = failures.into_iter().map(|r| r.unwrap_err()).collect();

    println!("parsed: {:?}", values);              // parsed: [10, 30, 50]
    println!("failed inputs: {:?}", errors.iter().map(|(s, _)| s).collect::<Vec<_>>());
    // failed inputs: ["bad", "?"]
}
Converting Between Result and Option

Result and Option are related types and Rust provides conversions in both directions.

RUST
fn main() {
    // Result -> Option: discard the error, keep only Ok as Some
    let ok: Result<i32, &str> = Ok(42);
    let as_option: Option<i32> = ok.ok();
    println!("{:?}", as_option); // Some(42)

    let err: Result<i32, &str> = Err("failed");
    let as_none: Option<i32> = err.ok();
    println!("{:?}", as_none); // None

    // err() — convert to Option<E>, keeping only the error
    let err2: Result<i32, &str> = Err("oops");
    let error_option: Option<&str> = err2.err();
    println!("{:?}", error_option); // Some("oops")

    // Option -> Result: provide an error for the None case
    let some_value: Option<i32> = Some(7);
    let as_result: Result<i32, &str> = some_value.ok_or("no value");
    println!("{:?}", as_result); // Ok(7)

    let none_value: Option<i32> = None;
    let as_err: Result<i32, &str> = none_value.ok_or("no value");
    println!("{:?}", as_err); // Err("no value")
}
Creating Your Own Error Types

For library and application code, define a custom error type so callers can match on specific failure modes. At minimum, implement std::fmt::Display and std::error::Error. Derive Debug as well.

RUST
use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    MissingKey(String),
    InvalidValue { key: String, reason: String },
    ParseError(ParseIntError),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigError::MissingKey(k) =>
                write!(f, "missing required config key: {}", k),
            ConfigError::InvalidValue { key, reason } =>
                write!(f, "invalid value for '{}': {}", key, reason),
            ConfigError::ParseError(e) =>
                write!(f, "parse error: {}", e),
        }
    }
}

impl std::error::Error for ConfigError {}

impl From<ParseIntError> for ConfigError {
    fn from(e: ParseIntError) -> Self {
        ConfigError::ParseError(e)
    }
}

fn get_port(raw: Option<&str>) -> Result<u16, ConfigError> {
    let value = raw.ok_or_else(|| ConfigError::MissingKey(String::from("port")))?;
    let port: u32 = value.parse().map_err(|e: ParseIntError| ConfigError::ParseError(e))?;
    if port > 65535 {
        return Err(ConfigError::InvalidValue {
            key:    String::from("port"),
            reason: format!("{} exceeds maximum port number", port),
        });
    }
    Ok(port as u16)
}

fn main() {
    println!("{:?}", get_port(Some("8080")));  // Ok(8080)
    println!("{:?}", get_port(None));           // Err(MissingKey("port"))
    println!("{:?}", get_port(Some("abc")));   // Err(ParseError(...))
    println!("{:?}", get_port(Some("99999"))); // Err(InvalidValue { ... })
}
Note
For richer error handling in larger projects, consider the `thiserror` crate for defining custom errors and `anyhow` for ergonomic error propagation in application code. They build directly on the standard error traits shown above.
Chaining Results for Clean Pipelines

Combining ?, map, and_then, and map_err lets you write linear, readable error-handling pipelines instead of deeply nested match blocks.

RUST
use std::collections::HashMap;

#[derive(Debug)]
enum PipelineError {
    Missing(String),
    Parse(String),
    OutOfRange(i32),
}

fn get_config_value<'a>(
    config: &'a HashMap<&str, &str>,
    key: &str,
) -> Result<&'a str, PipelineError> {
    config.get(key)
          .copied()
          .ok_or_else(|| PipelineError::Missing(key.to_string()))
}

fn parse_timeout(config: &HashMap<&str, &str>) -> Result<u32, PipelineError> {
    get_config_value(config, "timeout")
        .and_then(|s| {
            s.parse::<i32>()
             .map_err(|e| PipelineError::Parse(e.to_string()))
        })
        .and_then(|n| {
            if n > 0 && n <= 300 {
                Ok(n as u32)
            } else {
                Err(PipelineError::OutOfRange(n))
            }
        })
}

fn main() {
    let mut config = HashMap::new();
    config.insert("timeout", "30");

    println!("{:?}", parse_timeout(&config)); // Ok(30)

    config.insert("timeout", "999");
    println!("{:?}", parse_timeout(&config)); // Err(OutOfRange(999))

    let empty: HashMap<&str, &str> = HashMap::new();
    println!("{:?}", parse_timeout(&empty));  // Err(Missing("timeout"))
}
Result Methods at a Glance

Method

Returns

Description

unwrap()

T

Returns Ok value; panics on Err

expect("msg")

T

Like unwrap but with a custom panic message

unwrap_or(default)

T

Returns Ok value or a fixed fallback

unwrap_or_else(|e| ...)

T

Returns Ok value or calls closure with Err

unwrap_or_default()

T

Returns Ok value or T::default()

is_ok()

bool

True if the result is Ok

is_err()

bool

True if the result is Err

map(|v| ...)

Result<U,E>

Transforms Ok value; Err passes through

map_err(|e| ...)

Result<T,F>

Transforms Err value; Ok passes through

and_then(|v| ...)

Result<U,E>

Chains a fallible operation on Ok; Err short-circuits

or(other)

Result<T,F>

Returns self if Ok, otherwise other

or_else(|e| ...)

Result<T,F>

Returns self if Ok, otherwise calls closure

ok()

Option<T>

Converts Ok to Some, Err to None

err()

Option<E>

Converts Err to Some, Ok to None

? operator

T (or returns)

Unwraps Ok or returns Err from current function

Success
You now have a thorough understanding of Rust's Result type. Result<T, E> makes errors explicit in the type system — Ok(T) signals success, Err(E) signals failure, and the compiler ensures every caller handles both. Use match or if let for explicit handling, unwrap_or and unwrap_or_else for safe fallbacks, map and and_then for building clean transformation pipelines, and ? to propagate errors up the call stack without boilerplate. Combined with custom error types, Result gives you precise, composable, and zero-overhead error handling.