Rust error handling
last modified October 25, 2026
In this article we show how to handle errors in Rust.
Rust has no exceptions. It divides errors into two groups.
Unrecoverable errors are bugs, such as an index past the end of an
array; they stop the program with a panic.
Recoverable errors, such as a missing file or invalid user input,
are expected to happen and are returned as values of the
Result<T, E> type.
enum Result<T, E> {
Ok(T),
Err(E),
}
A function that can fail returns Ok with the value on success,
or Err with an error. Because the error is part of the return
type, the caller cannot forget about it: the value is accessible only after
the Result has been checked.
The examples were tested with Rust 1.97 and the 2024 edition. They are run as
Cargo projects with cargo run -q.
Panics
A panic happens when the program reaches a state it cannot handle. The standard library panics, for example, on out-of-bounds indexing.
fn main() {
let vals: Vec<i32> = (1..=3).collect();
let idx = 5;
println!("accessing element {idx}");
let v = vals[idx];
println!("value: {v}");
}
We access an element past the end of a vector.
let v = vals[idx];
The indexing operator panics, because the vector has only three elements.
The panic message with its source location is printed to standard error, the
stack is unwound, and the program terminates with exit code 101. The last
println! is never reached.
The number in parentheses after the thread name is the thread ID; it differs between runs.
$ cargo run -q accessing element 5 thread 'main' (35831) panicked at src/main.rs:6:17: index out of bounds: the len is 3 but the index is 5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace $ echo $? 101
The panic! macro
Our own code can panic with the panic! macro.
fn average(vals: &[f64]) -> f64 {
if vals.is_empty() {
panic!("cannot compute the average of an empty slice");
}
vals.iter().sum::<f64>() / vals.len() as f64
}
fn main() {
println!("{}", average(&[2.0, 4.0, 9.0]));
println!("{}", average(&[]));
}
The average function panics when it gets an empty slice.
if vals.is_empty() {
panic!("cannot compute the average of an empty slice");
}
The macro takes a message in the same format as println!. A
panic is appropriate when the caller has broken the function's contract,
that is, for a programming error. Errors that the caller can reasonably
expect and handle should be returned as a Result instead.
$ cargo run -q 5 thread 'main' (35849) panicked at src/main.rs:3:9: cannot compute the average of an empty slice note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The Result type
Many standard functions return a Result. String parsing is a
typical example.
fn main() {
let inputs = ["42", "-17", "3.14", "abc", "99999999999"];
for s in inputs {
let res: Result<i32, _> = s.parse::<i32>();
match res {
Ok(n) => println!("{s:>12} -> {n}"),
Err(e) => println!("{s:>12} -> error: {e}"),
}
}
}
We parse several strings into integers.
let res: Result<i32, _> = s.parse::<i32>();
The parse method returns Result<i32,
ParseIntError>.
match res {
Ok(n) => println!("{s:>12} -> {n}"),
Err(e) => println!("{s:>12} -> error: {e}"),
}
A match handles both outcomes. The error value implements
Display and describes what went wrong.
$ cargo run -q
42 -> 42
-17 -> -17
3.14 -> error: invalid digit found in string
abc -> error: invalid digit found in string
99999999999 -> error: number too large to fit in target type
Returning Result from a function
Our own functions report failures by returning a Result.
fn sqrt_checked(x: f64) -> Result<f64, String> {
if x < 0.0 {
Err(format!("cannot take the square root of {x}"))
} else {
Ok(x.sqrt())
}
}
fn main() {
for x in [16.0, 2.0, -4.0] {
match sqrt_checked(x) {
Ok(r) => println!("sqrt({x}) = {r:.4}"),
Err(msg) => println!("error: {msg}"),
}
}
if let Err(e) = sqrt_checked(-1.0) {
println!("failed: {e}");
}
println!("{}", sqrt_checked(9.0).is_ok());
}
The sqrt_checked function refuses negative numbers.
fn sqrt_checked(x: f64) -> Result<f64, String> {
if x < 0.0 {
Err(format!("cannot take the square root of {x}"))
} else {
Ok(x.sqrt())
}
}
The error type can be any type. A String is the simplest choice
for small programs; later we show dedicated error types that callers can
inspect.
if let Err(e) = sqrt_checked(-1.0) {
println!("failed: {e}");
}
When only one outcome is interesting, if let is shorter than a
match. The is_ok and is_err methods
just test the variant.
$ cargo run -q sqrt(16) = 4.0000 sqrt(2) = 1.4142 error: cannot take the square root of -4 failed: cannot take the square root of -1 true
unwrap, expect, and defaults
Several methods extract the value from a Result without a
match.
fn main() {
let a: i32 = "12".parse().unwrap();
let b: i32 = "7".parse().expect("b must be a number");
println!("{}", a + b);
let c: i32 = "x".parse().unwrap_or(0);
let d: i32 = "".parse().unwrap_or_default();
let e: i32 = "y".parse().unwrap_or_else(|err| {
println!("using fallback because: {err}");
-1
});
println!("{c} {d} {e}");
let port: u16 = "70000".parse().expect("port must be between 0 and 65535");
println!("{port}");
}
We use different strategies for the error case.
let a: i32 = "12".parse().unwrap();
let b: i32 = "7".parse().expect("b must be a number");
The unwrap method returns the value or panics on an error;
expect does the same with our own message. They are useful in
examples, tests, and in cases where an error would be a bug.
let c: i32 = "x".parse().unwrap_or(0); let d: i32 = "".parse().unwrap_or_default();
unwrap_or returns a fallback value on error and
unwrap_or_default returns the default value of the type.
unwrap_or_else computes the fallback with a closure, which
receives the error.
let port: u16 = "70000".parse().expect("port must be between 0 and 65535");
The value does not fit into a u16, so expect
panics. The message is followed by the Debug representation of
the error. More on these methods is in
Rust unwrap function.
$ cargo run -q
19
using fallback because: invalid digit found in string
0 0 -1
thread 'main' (35882) panicked at src/main.rs:14:37:
port must be between 0 and 65535: ParseIntError { kind: PosOverflow }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The ? operator
The ? operator propagates errors to the caller. It is the most
common way of handling errors in Rust functions.
use std::num::ParseIntError;
fn sum_pair(s: &str) -> Result<i32, ParseIntError> {
let (a, b) = s.split_once(',').unwrap_or((s, "0"));
let x: i32 = a.trim().parse()?;
let y: i32 = b.trim().parse()?;
Ok(x + y)
}
fn main() {
for s in ["3, 4", "10,-2", "8", "5, five"] {
match sum_pair(s) {
Ok(v) => println!("{s:?} -> {v}"),
Err(e) => println!("{s:?} -> error: {e}"),
}
}
}
The sum_pair function parses two comma-separated numbers.
let x: i32 = a.trim().parse()?; let y: i32 = b.trim().parse()?; Ok(x + y)
If parse returns Ok, the ? operator
unwraps the value. If it returns Err, the function returns
immediately with that error. The operator can be used only in functions whose
return type is compatible, here Result<i32,
ParseIntError>.
The line let x: i32 = a.trim().parse()?; is roughly equivalent
to the following code:
let x: i32 = match a.trim().parse() {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
};
The call to From::from converts the error into the error type
of the function; we use this conversion later with custom error types.
$ cargo run -q "3, 4" -> 7 "10,-2" -> 8 "8" -> 8 "5, five" -> error: invalid digit found in string
The ? operator with Option
The ? operator also works in functions that return
Option. There, it returns None early.
fn first_initial(full_name: &str) -> Option<char> {
let last = full_name.split_whitespace().nth(1)?;
let c = last.chars().next()?;
Some(c.to_ascii_uppercase())
}
fn main() {
println!("{:?}", first_initial("John doe"));
println!("{:?}", first_initial("Plato"));
let age = "abc".parse::<u32>().ok();
println!("{:?}", age);
let name: Option<&str> = None;
let r: Result<&str, String> = name.ok_or(String::from("name is missing"));
println!("{:?}", r);
}
The first_initial function returns the uppercased initial of
the second word.
let last = full_name.split_whitespace().nth(1)?; let c = last.chars().next()?;
If the name has no second word, the function returns None.
let age = "abc".parse::<u32>().ok();
let r: Result<&str, String> = name.ok_or(String::from("name is missing"));
The ? operator does not convert between Option and
Result automatically. The ok method turns a
Result into an Option, discarding the error, and
ok_or turns an Option into a Result
with the given error.
$ cargo run -q
Some('D')
None
None
Err("name is missing")
Returning Result from main
The main function can return a Result, which
allows using ? in it.
use std::fs;
use std::io;
fn main() -> Result<(), io::Error> {
fs::write("words.txt", "sky\nblue\nrock\n")?;
let content = fs::read_to_string("words.txt")?;
println!("{} lines", content.lines().count());
let other = fs::read_to_string("missing.txt")?;
println!("{other}");
Ok(())
}
The program writes a file, reads it back, and then tries to read a file that does not exist.
fn main() -> Result<(), io::Error> {
On success, main returns Ok(()). If it returns
an Err, Rust prints the error with its Debug
representation, prefixed with Error:, and the process exits
with code 1.
let other = fs::read_to_string("missing.txt")?;
Reading the missing file fails, and the ? operator returns the
error from main.
$ cargo run -q
3 lines
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
$ echo $?
1
Box<dyn Error>
A function often calls operations that fail with different error types. The
Box<dyn Error> type can hold any error.
use std::error::Error;
use std::fs;
fn total(path: &str) -> Result<i64, Box<dyn Error>> {
let content = fs::read_to_string(path)?;
let mut sum = 0;
for line in content.lines() {
let n: i64 = line.trim().parse()?;
sum += n;
}
if sum < 0 {
return Err("the total must not be negative".into());
}
Ok(sum)
}
fn main() {
fs::write("good.txt", "10\n20\n30\n").unwrap();
fs::write("bad.txt", "10\ntwenty\n30\n").unwrap();
fs::write("neg.txt", "10\n-50\n").unwrap();
for path in ["good.txt", "bad.txt", "neg.txt", "none.txt"] {
match total(path) {
Ok(t) => println!("{path}: total {t}"),
Err(e) => println!("{path}: error: {e}"),
}
}
}
The total function reads numbers from a file and sums them. It
can fail with an I/O error, a parse error, or our own error.
fn total(path: &str) -> Result<i64, Box<dyn Error>> {
Box<dyn Error> is a trait object for any type that
implements the std::error::Error trait. The ?
operator converts both io::Error and
ParseIntError into it.
return Err("the total must not be negative".into());
A string slice can be converted into a Box<dyn Error>
with into. This is a quick way to create an ad hoc error.
This approach is convenient for applications and prototypes. Its drawback is that callers cannot easily find out which error happened; they get only a message. Libraries usually define their own error types.
$ cargo run -q good.txt: total 60 bad.txt: error: invalid digit found in string neg.txt: error: the total must not be negative none.txt: error: No such file or directory (os error 2)
Custom error types
A custom error type, usually an enum, lets callers distinguish between failure cases.
use std::error::Error;
use std::fmt;
#[derive(Debug)]
enum WithdrawError {
InsufficientFunds { needed: u64, available: u64 },
AccountLocked,
ZeroAmount,
}
impl fmt::Display for WithdrawError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
WithdrawError::InsufficientFunds { needed, available } => {
write!(f, "insufficient funds: need {needed}, have {available}")
}
WithdrawError::AccountLocked => write!(f, "the account is locked"),
WithdrawError::ZeroAmount => write!(f, "amount must be greater than zero"),
}
}
}
impl Error for WithdrawError {}
struct Account {
balance: u64,
locked: bool,
}
impl Account {
fn withdraw(&mut self, amount: u64) -> Result<u64, WithdrawError> {
if self.locked {
return Err(WithdrawError::AccountLocked);
}
if amount == 0 {
return Err(WithdrawError::ZeroAmount);
}
if amount > self.balance {
return Err(WithdrawError::InsufficientFunds {
needed: amount,
available: self.balance,
});
}
self.balance -= amount;
Ok(self.balance)
}
}
fn main() {
let mut acc = Account { balance: 100, locked: false };
for amount in [30, 0, 90, 70] {
match acc.withdraw(amount) {
Ok(left) => println!("withdrew {amount}, left {left}"),
Err(WithdrawError::InsufficientFunds { needed, .. }) if needed > 80 => {
println!("{amount} is too much; try a smaller amount")
}
Err(e) => println!("error: {e}"),
}
}
acc.locked = true;
if let Err(e) = acc.withdraw(10) {
println!("error: {e} ({e:?})");
}
}
Withdrawing money from an account can fail for three reasons.
#[derive(Debug)]
enum WithdrawError {
InsufficientFunds { needed: u64, available: u64 },
AccountLocked,
ZeroAmount,
}
Each failure is a variant, and variants can carry details about the error.
impl fmt::Display for WithdrawError {
...
impl Error for WithdrawError {}
An error type should implement Debug, Display, and
the Error trait. All methods of Error have default
implementations, so the impl block can be empty. Implementing
Error makes the type usable with Box<dyn
Error>.
Err(WithdrawError::InsufficientFunds { needed, .. }) if needed > 80 => {
println!("{amount} is too much; try a smaller amount")
}
Callers can match on specific variants and react to them, here with a guard.
Other errors are printed with their Display text.
$ cargo run -q withdrew 30, left 70 error: amount must be greater than zero 90 is too much; try a smaller amount withdrew 70, left 0 error: the account is locked (AccountLocked)
Converting errors with From
When a function returns a custom error type, the ? operator can
convert other errors into it through the From trait.
use std::fmt;
use std::num::{ParseFloatError, ParseIntError};
#[derive(Debug)]
enum ConfigError {
MissingKey(String),
BadInt(ParseIntError),
BadFloat(ParseFloatError),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::MissingKey(k) => write!(f, "missing key '{k}'"),
ConfigError::BadInt(e) => write!(f, "invalid integer: {e}"),
ConfigError::BadFloat(e) => write!(f, "invalid float: {e}"),
}
}
}
impl std::error::Error for ConfigError {}
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> Self {
ConfigError::BadInt(e)
}
}
impl From<ParseFloatError> for ConfigError {
fn from(e: ParseFloatError) -> Self {
ConfigError::BadFloat(e)
}
}
struct Config {
workers: u32,
ratio: f64,
}
fn get<'a>(text: &'a str, key: &str) -> Result<&'a str, ConfigError> {
text.lines()
.filter_map(|ln| ln.split_once('='))
.find(|(k, _)| k.trim() == key)
.map(|(_, v)| v.trim())
.ok_or_else(|| ConfigError::MissingKey(key.to_string()))
}
fn parse_config(text: &str) -> Result<Config, ConfigError> {
let workers = get(text, "workers")?.parse()?;
let ratio = get(text, "ratio")?.parse()?;
Ok(Config { workers, ratio })
}
fn main() {
let inputs = [
"workers = 4\nratio = 0.75",
"workers = four\nratio = 0.75",
"workers = 4\nratio = high",
"workers = 4",
];
for text in inputs {
match parse_config(text) {
Ok(c) => println!("workers {}, ratio {}", c.workers, c.ratio),
Err(e) => println!("error: {e}"),
}
}
}
We parse a simple configuration in the key = value format.
enum ConfigError {
MissingKey(String),
BadInt(ParseIntError),
BadFloat(ParseFloatError),
}
The error type wraps the errors of the standard library functions it uses.
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> Self {
ConfigError::BadInt(e)
}
}
This implementation tells Rust how to turn a ParseIntError
into a ConfigError.
let workers = get(text, "workers")?.parse()?; let ratio = get(text, "ratio")?.parse()?;
The get function returns a ConfigError directly.
The parse calls return standard errors, which the
? operator converts with our From implementations.
The target types of parse are inferred from the fields of
Config.
$ cargo run -q workers 4, ratio 0.75 error: invalid integer: invalid digit found in string error: invalid float: invalid float literal error: missing key 'ratio'
Mapping errors
The map_err method converts an error into another type without
a From implementation. It is handy when the conversion needs
extra context.
#[derive(Debug)]
struct AppError {
msg: String,
}
fn parse_age(s: &str) -> Result<u8, AppError> {
let age: u8 = s
.trim()
.parse()
.map_err(|e| AppError { msg: format!("'{s}' is not a valid age: {e}") })?;
if age > 150 {
return Err(AppError { msg: format!("{age} is not realistic") });
}
Ok(age)
}
fn main() {
for s in ["34", "-3", "200", "151"] {
match parse_age(s) {
Ok(a) => println!("age: {a}"),
Err(e) => println!("{}", e.msg),
}
}
}
We parse an age and add the input to the error message.
let age: u8 = s
.trim()
.parse()
.map_err(|e| AppError { msg: format!("'{s}' is not a valid age: {e}") })?;
The closure receives the original ParseIntError and returns an
AppError. Then ? propagates it. The input
"200" fits into a u8 and is rejected by our own
check.
$ cargo run -q age: 34 '-3' is not a valid age: invalid digit found in string 200 is not realistic 151 is not realistic
Result combinators
Like Option, Result has methods that transform the
value or the error without explicit matching.
fn main() {
let r: Result<i32, String> = Ok(5);
let doubled = r.clone().map(|n| n * 2);
println!("{:?}", doubled);
let bad: Result<i32, String> = Err(String::from("no value"));
println!("{:?}", bad.clone().map(|n| n * 2));
println!("{:?}", bad.map_err(|e| e.len()));
let checked = "8"
.parse::<i32>()
.map_err(|e| e.to_string())
.and_then(|n| if n % 2 == 0 { Ok(n / 2) } else { Err(format!("{n} is odd")) });
println!("{:?}", checked);
let fallback = "x".parse::<i32>().or_else(|_| "10".parse::<i32>());
println!("{:?}", fallback);
println!("{}", r.is_ok_and(|n| n > 3));
}
We chain several operations on Result values.
let doubled = r.clone().map(|n| n * 2);
map transforms the Ok value and leaves an
Err untouched; map_err does the opposite. The
methods consume the Result, so we clone it to use it again
later.
.and_then(|n| if n % 2 == 0 { Ok(n / 2) } else { Err(format!("{n} is odd")) });
and_then chains an operation that can itself fail. The error
types must match, which is why we first convert the
ParseIntError to a String.
let fallback = "x".parse::<i32>().or_else(|_| "10".parse::<i32>());
or_else tries an alternative when the first operation fails.
is_ok_and tests the value with a predicate.
$ cargo run -q
Ok(10)
Err("no value")
Err(8)
Ok(4)
Ok(10)
true
Errors in iterators
When processing a collection, each element can produce an error. Iterators offer several ways to deal with that.
use std::num::ParseIntError;
fn main() {
let good = ["1", "2", "3"];
let bad = ["1", "two", "3", "four"];
let a: Result<Vec<i32>, ParseIntError> = good.iter().map(|s| s.parse()).collect();
println!("{:?}", a);
let b: Result<Vec<i32>, ParseIntError> = bad.iter().map(|s| s.parse()).collect();
println!("{:?}", b);
let (oks, errs): (Vec<_>, Vec<_>) = bad
.iter()
.map(|s| s.parse::<i32>())
.partition(|r| r.is_ok());
let nums: Vec<i32> = oks.into_iter().map(Result::unwrap).collect();
println!("parsed {:?}, {} errors", nums, errs.len());
let valid: Vec<i32> = bad.iter().filter_map(|s| s.parse().ok()).collect();
println!("{:?}", valid);
let sum: Result<i32, _> = good.iter().map(|s| s.parse::<i32>()).sum();
println!("{:?}", sum);
}
We parse two lists of strings.
let b: Result<Vec<i32>, ParseIntError> = bad.iter().map(|s| s.parse()).collect();
Collecting an iterator of Result values into
Result<Vec<_>, _> gives Ok with all
values, or the first error. The iteration stops at the first error.
let (oks, errs): (Vec<_>, Vec<_>) = bad
.iter()
.map(|s| s.parse::<i32>())
.partition(|r| r.is_ok());
To keep processing after errors, partition splits the results
into successes and failures.
let valid: Vec<i32> = bad.iter().filter_map(|s| s.parse().ok()).collect();
filter_map with ok silently skips the invalid
elements. sum and product also accept
Result items and stop at the first error.
$ cargo run -q
Ok([1, 2, 3])
Err(ParseIntError { kind: InvalidDigit })
parsed [1, 3], 2 errors
[1, 3]
Ok(6)
Error sources
An error can wrap a lower-level error that caused it. The
source method of the Error trait exposes the cause,
so that a chain of errors can be reported.
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
struct LineError {
line: usize,
source: ParseIntError,
}
impl fmt::Display for LineError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "bad value on line {}", self.line)
}
}
impl Error for LineError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
fn parse_lines(text: &str) -> Result<Vec<i32>, LineError> {
text.lines()
.enumerate()
.map(|(i, ln)| ln.parse().map_err(|e| LineError { line: i + 1, source: e }))
.collect()
}
fn report(e: &dyn Error) {
println!("error: {e}");
let mut cause = e.source();
while let Some(c) = cause {
println!(" caused by: {c}");
cause = c.source();
}
}
fn main() {
match parse_lines("4\n8\n15") {
Ok(v) => println!("{:?}", v),
Err(e) => report(&e),
}
match parse_lines("4\n8\n1x5\n16") {
Ok(v) => println!("{:?}", v),
Err(e) => report(&e),
}
}
The LineError type adds the line number to a parse error.
impl Error for LineError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
We override the default source method, which returns
None, to return the wrapped ParseIntError. The
Display text of LineError describes only its own
level; the cause is reported separately.
let mut cause = e.source();
while let Some(c) = cause {
println!(" caused by: {c}");
cause = c.source();
}
The report function walks the chain of causes. Popular crates
make this pattern shorter: thiserror derives the
Display, Error, and From
implementations for custom error types, and anyhow provides a
convenient error type for applications with context messages.
$ cargo run -q [4, 8, 15] error: bad value on line 3 caused by: invalid digit found in string
Source
The Rust Programming Language: Error Handling
In this article we have handled errors in Rust. The Option type
is covered in Rust Option type.
Author
List all Rust tutorials.