The Result Enum
Real programs fail in ordinary, expected ways: a file might not exist, a network request might time out, a string might not be a valid number. Rust has no exceptions and no null value to signal these failures. Instead it has Result<T, E>, an enum that forces you to acknowledge, at compile time, that an operation can either succeed or fail. Once you understand Result, you can read almost any real-world Rust function signature and know exactly what can go wrong and what you’re expected to do about it.
Overview / How it works
Result<T, E> is defined in the standard library as an enum with exactly two variants: Ok(T), which wraps a success value of type T, and Err(E), which wraps an error value of type E. Both T and E are generic — a function returning Result<i32, String> succeeds with an i32 or fails with a String describing the problem; a function returning Result<File, std::io::Error> succeeds with an open File or fails with an io::Error. The two type parameters let every fallible function in the standard library, and every fallible function you write, describe precisely what it returns on success and what it returns on failure, using the type system rather than documentation alone.
Compare this to languages you may already know. In Python or Java, a failing operation typically throws an exception that unwinds the call stack until something catches it — and nothing in a function’s signature tells you it can throw, or what it can throw. In C, a function might return -1 or set a global errno, and it’s easy to forget to check. Rust takes a third approach: failure is just another return value. A function that can fail returns a Result, and the compiler will not let you silently ignore it. You cannot accidentally treat an Err as if it were the success value, because the two are different enum variants — to get at the inner value you must handle both cases, typically with match or one of Result‘s many combinator methods.
This matters for how Rust manages memory and safety without a garbage collector. There is no hidden control flow: when a function returns a Result, that value is owned by the caller like any other value, following the same ownership and move rules as every other type. There’s no stack unwinding to reason about (aside from the separate, much rarer mechanism of a panic!, which is for unrecoverable bugs, not everyday failures). This is why Rust’s error-handling story pairs so naturally with its ownership model: a Result is data, and data follows the rules you already know.
A quick mental model: think of Result<T, E> as a labeled box. Before you can use what’s inside, you have to look at the label. If it says Ok, there’s a T inside and you can use it. If it says Err, there’s an E inside describing what went wrong, and there is no T to use at all — the compiler will refuse to let you pretend otherwise.
Syntax
The standard library defines Result roughly like this (shown here for reference, not something you write yourself):
enum Result<T, E> {
Ok(T),
Err(E),
}
A typical fallible function signature looks like this:
fn do_something(input: &str) -> Result<SuccessType, ErrorType> {
// ...
}
T— the type produced when the operation succeeds, wrapped inOk.E— the type produced when the operation fails, wrapped inErr.- The caller must handle both variants (via
match,if let, or a combinator) before it can use the success value.
Result comes with many built-in methods for working with it without always writing a full match:
| Method | Behavior |
|---|---|
is_ok() / is_err() |
Returns true/false without consuming the Result. |
unwrap() |
Returns the Ok value, or panics if it’s an Err. |
expect("message") |
Like unwrap(), but panics with your custom message — better for diagnostics. |
unwrap_or(default) |
Returns the Ok value, or a fallback default if it’s an Err. |
unwrap_or_else(f) |
Like unwrap_or, but computes the fallback lazily from a closure. |
map(f) |
Transforms the Ok value with f, leaves Err untouched. |
map_err(f) |
Transforms the Err value with f, leaves Ok untouched. |
and_then(f) |
Chains another Result-returning operation if this one succeeded. |
ok() / err() |
Converts to Option<T> / Option<E>, discarding the other side. |
? operator |
Inside a function returning a compatible Result, unwraps Ok or returns the Err early. |
Examples
Example 1: A basic fallible function
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0.0 {
Err(String::from("division by zero"))
} else {
Ok(numerator / denominator)
}
}
fn main() {
let result = divide(10.0, 2.0);
match result {
Ok(value) => println!("Result: {}", value),
Err(e) => println!("Error: {}", e),
}
let result2 = divide(5.0, 0.0);
match result2 {
Ok(value) => println!("Result: {}", value),
Err(e) => println!("Error: {}", e),
}
}
Output:
Result: 5
Error: division by zero
divide never panics and never returns a nonsense number like NaN or -1 to mean “failed.” It returns Ok with the real quotient, or Err with a String explaining the problem. The caller is forced by the type system to match on the result before it can print a value — there is no way to accidentally print an error as if it were a valid quotient.
Example 2: Using the standard library’s error types with ?
use std::num::ParseIntError;
fn parse_and_double(input: &str) -> Result<i32, ParseIntError> {
let number: i32 = input.parse()?;
Ok(number * 2)
}
fn main() {
match parse_and_double("21") {
Ok(value) => println!("Doubled: {}", value),
Err(e) => println!("Failed to parse: {}", e),
}
match parse_and_double("abc") {
Ok(value) => println!("Doubled: {}", value),
Err(e) => println!("Failed to parse: {}", e),
}
}
Output:
Doubled: 42
Failed to parse: invalid digit found in string
str::parse returns Result<i32, ParseIntError> — it can fail if the string isn’t a valid integer. Inside parse_and_double, the ? operator says: “if this is Ok, give me the inner i32 and keep going; if it’s Err, stop right here and return that Err from parse_and_double immediately.” This only compiles because parse_and_double‘s own return type, Result<i32, ParseIntError>, is compatible with the error type ? is propagating — that requirement is enforced by the compiler.
Example 3: Chaining fallible steps with a custom error type
use std::fmt;
#[derive(Debug)]
enum MathError {
DivisionByZero,
NegativeSquareRoot,
}
impl fmt::Display for MathError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MathError::DivisionByZero => write!(f, "cannot divide by zero"),
MathError::NegativeSquareRoot => write!(f, "cannot take square root of a negative number"),
}
}
}
fn safe_divide(a: f64, b: f64) -> Result<f64, MathError> {
if b == 0.0 {
Err(MathError::DivisionByZero)
} else {
Ok(a / b)
}
}
fn safe_sqrt(x: f64) -> Result<f64, MathError> {
if x < 0.0 {
Err(MathError::NegativeSquareRoot)
} else {
Ok(x.sqrt())
}
}
fn divide_then_sqrt(a: f64, b: f64) -> Result<f64, MathError> {
let quotient = safe_divide(a, b)?;
let root = safe_sqrt(quotient)?;
Ok(root)
}
fn main() {
match divide_then_sqrt(16.0, 4.0) {
Ok(value) => println!("Result: {}", value),
Err(e) => println!("Error: {}", e),
}
match divide_then_sqrt(16.0, 0.0) {
Ok(value) => println!("Result: {}", value),
Err(e) => println!("Error: {}", e),
}
match divide_then_sqrt(-16.0, 4.0) {
Ok(value) => println!("Result: {}", value),
Err(e) => println!("Error: {}", e),
}
}
Output:
Result: 2
Error: cannot divide by zero
Error: cannot take square root of a negative number
This is closer to real code: two fallible steps are chained, and either one can fail with its own reason. MathError is a small custom enum implementing Display so it prints a readable message. Inside divide_then_sqrt, each ? either unwraps the previous step’s Ok value or exits the whole function early with the Err it received — no nested match pyramids required.
How it works step by step
Take the third call, divide_then_sqrt(-16.0, 4.0), and trace it exactly as the compiler and runtime see it:
safe_divide(-16.0, 4.0)runs.bis4.0, not zero, so it returnsOk(-4.0).- Back in
divide_then_sqrt, the linelet quotient = safe_divide(a, b)?;evaluates theResult. Because it’sOk(-4.0),?unwraps it and bindsquotient = -4.0. Execution continues to the next line — nothing special happens on theOkpath except unwrapping. safe_sqrt(-4.0)runs. Since-4.0 < 0.0is true, it returnsErr(MathError::NegativeSquareRoot).- Back in
divide_then_sqrt,let root = safe_sqrt(quotient)?;now sees anErr. This is where?does its real work: it immediately returnsErr(MathError::NegativeSquareRoot)fromdivide_then_sqrt— the line after it,Ok(root), never runs. - The caller’s
matchinmainreceives thatErr, binds it toe, and callsprintln!("Error: {}", e), which invokesMathError‘sDisplayimplementation to print"cannot take square root of a negative number".
Crucially, all of this happens through ordinary function returns — there’s no stack unwinding, no hidden jump. The ? operator is syntactic sugar the compiler expands roughly into a match that either unwraps Ok or does an early return Err(...). That’s also why ? can only be used inside a function whose own return type is a compatible Result (or Option) — the compiler needs somewhere valid to return that Err to.
Common Mistakes
Mistake 1: Calling .unwrap() on a Result that can fail
.unwrap() is convenient, but it panics immediately if the value is an Err — it does not print your own error message, it does not let the caller recover, and it crashes the whole program at that point:
let input = "not_a_number";
let number: i32 = input.parse().unwrap();
println!("{}", number);
This compiles fine, but running it panics before the println! ever executes, with a message like called \`Result::unwrap()\` on an \`Err\` value: ParseIntError { kind: InvalidDigit }. Prefer handling the failure explicitly:
fn main() {
let input = "not_a_number";
match input.parse::<i32>() {
Ok(number) => println!("Parsed: {}", number),
Err(_) => println!("Could not parse '{}' as a number", input),
}
}
Output:
Could not parse 'not_a_number' as a number
Reserve .unwrap()/.expect() for cases where an Err genuinely can’t happen (and even then, .expect("why") is better than a bare .unwrap() because it documents that assumption), or for quick prototypes and tests.
Mistake 2: Using ? where the enclosing function doesn’t return a compatible type
The ? operator needs somewhere to return an Err to. A plain fn main() { ... } returns (), not a Result, so this fails to compile:
fn main() {
let number: i32 = "42".parse()?; // error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option`
println!("{}", number);
}
The compiler rejects this with an error pointing at the ?. The fix is to give main a return type that ? can propagate into — Rust specifically allows main to return Result<(), E> for exactly this situation:
fn main() -> Result<(), std::num::ParseIntError> {
let number: i32 = "42".parse()?;
println!("{}", number);
Ok(())
}
Output:
42
If main returns Err, the process exits with a nonzero status and prints the error via Debug; returning Ok(()) exits normally. For functions other than main, the fix is the same idea: give the function a Result return type that matches (or can absorb, via From) the error type you’re propagating.
Best Practices
- Prefer
?over manualmatchchains when you just want to propagate an error unchanged — it keeps the success path readable. - Avoid
.unwrap()and.expect()in library and production code paths; reserve them for tests, quick scripts, or situations where failure is truly impossible, and explain why in the message passed to.expect(). - Design function signatures so the error type tells the caller something useful — a custom enum with meaningful variants is more helpful than a bare
String. - Implement
std::fmt::Display(and oftenstd::error::Error) for custom error types so they print well and compose with other error-handling code. - Use
map,map_err, andand_thento transform and chainResults without leaving the “railway” of success/failure handling. - Don’t reach for
Resultto model “this value might be absent” with no notion of failure — that’s whatOption<T>is for; reserveResultfor operations that can genuinely fail with a reason. - In larger real-world projects, crates that make custom error types and error conversion less boilerplate-heavy are extremely common — but the core mental model is exactly what’s covered here.
Practice Exercises
- Write a function
fn checked_divide(a: i32, b: i32) -> Result<i32, String>that returnsErrwith a descriptive message whenbis zero, andOk(a / b)otherwise. Call it with a few pairs of numbers, including a zero divisor, and print the outcome withmatch. - Write a function
fn parse_all(input: &str) -> Result<Vec<i32>, std::num::ParseIntError>that splits a comma-separated string like"3,7,12"on commas and parses each piece into ani32, using?inside a loop so the whole function fails on the first bad piece. Test it with a valid string and one containing a non-numeric piece. - Define your own two-variant error enum (for example
enum ConfigError { Missing, Invalid(String) }), implementDisplayfor it, and write a function that returnsResult<i32, ConfigError>and can fail either way. Handle both variants distinctly in amatchand print a different message for each.
Summary
Result<T, E>is an enum with two variants,Ok(T)for success andErr(E)for failure, used throughout Rust instead of exceptions.- The compiler forces you to handle both variants before using the success value — you cannot silently ignore an error.
matchandif lethandle aResultexplicitly; combinators likemap,map_err,unwrap_or, andand_thentransform or chain it without a fullmatch.- The
?operator unwraps anOkor returns theErrearly from the current function, which must itself return a compatibleResult(orOption). .unwrap()and.expect()panic onErr— use them sparingly, and prefer explicit handling in real code.- Custom error enums with a
Displayimplementation communicate failure reasons far better than a bareStringor generic error.
