Error Handling in Rust
Demo
Example with anyhow
[package]
name = "with-anyhow"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.97"
use anyhow::bail;
fn main() -> anyhow::Result<()> {
let args = std::env::args().collect::<Vec<_>>();
if args.len() != 2 {
bail!("Usage: {} <number>", args[0]);
}
let number: i32 = args[1].parse().map_err(|_| anyhow::anyhow!("Invalid number"))?;
do_something(number)?;
Ok(())
}
fn do_something(answer: i32) -> anyhow::Result<()> {
println!("Doing something!");
if answer != 42 {
bail!("Invalid number: {}", answer);
}
println!("The answer is 42!");
Ok(())
}
[package]
name = "with-anyhow"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.97"
use anyhow::bail;
fn main() {
match do_something(42) {
Ok(_) => println!("Success!"),
Err(err) => println!("Error: {}", err),
}
match do_something(23) {
Ok(_) => println!("Success!"),
Err(err) => println!("Error: {}", err),
}
}
fn do_something(answer: i32) -> anyhow::Result<()> {
println!("Doing something!");
if answer != 42 {
bail!("An error occurred");
}
println!("The answer is 42!");
Ok(())
}
TODO: Error handling in Rust
[package]
name = "error-handling"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
fn main() {
let args = std::env::args().collect::<Vec<String>>();
if args.len() != 2 {
eprintln!("Usage: {} FILENAME", args[0]);
std::process::exit(1);
}
let filename = &args[1];
println!("{}", divide(filename));
}
fn divide(filename: &str) -> Result<i32, std::io::Error> {
let content = std::fs::read_to_string(filename)?;
let content = content.strip_suffix('\n').unwrap();
let (dividend, divisor) = content.split_once(',').unwrap();
let dividend = dividend.parse::<i32>().unwrap();
let divisor = divisor.parse::<i32>().unwrap();
dividend / divisor
}
// fn divide(filename: &str) -> i32 {
// let content = std::fs::read_to_string(filename).unwrap();
// let content = content.strip_suffix('\n').unwrap();
// let (dividend, divisor) = content.split_once(',').unwrap();
// let dividend = dividend.parse::<i32>().unwrap();
// let divisor = divisor.parse::<i32>().unwrap();
// dividend / divisor
// }
1
30,2
30,0
forty,two
Runtime error (panic) noticed during compilation
fn main() {
let a = 8;
let b = 2;
let c = 0;
let x = a / b;
println!("{x}");
#[allow(unconditional_panic)]
let y = a / c;
println!("{y}");
}
error: this operation will panic at runtime
--> examples/errors/div_by_zero_hard_coded.rs:8:13
|
8 | let y = a / c;
| ^^^^^ attempt to divide `8_i32` by zero
|
= note: `#[deny(unconditional_panic)]` on by default
error: aborting due to previous error
Divide by zero panick in function
fn div(a: i32, b: i32) -> i32 {
a / b
}
fn main() {
for number in [2, 4, 0, 20] {
let res = div(100, number);
dbg!(res);
}
}
Return error on failure
- match
- Ok
- Err
fn div(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
return Err("Cannot divide by 0");
}
Ok(a / b)
}
fn main() {
for number in [2, 4, 0, 20] {
let res = div(100, number);
match res {
Ok(result) => println!("{result}"),
Err(err) => println!("{err}"),
}
}
}
Divide by zero runtime panic
use std::io;
use std::io::Write;
fn main() {
loop {
let dividend = 100;
let divisor = get_number();
let res = divide_by(dividend, divisor);
println!("{dividend} / {divisor} = {res}");
}
}
fn divide_by(dividend: i32, divisor: i32) -> i32 {
dividend / divisor
}
fn get_number() -> i32 {
let mut number = String::new();
print!("Please type in a number: ");
io::stdout().flush().expect("Oups");
io::stdin()
.read_line(&mut number)
.expect("Failed to get input");
let number: i32 = number.trim().parse().expect("Could not convert to i32");
number
}
Please type in a number: 20
5
Please type in a number: 0
thread 'main' panicked at 'attempt to divide by zero', examples/errors/div_by_zero.rs:7:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Divide by zero return error
use std::io;
use std::io::Write;
fn main() {
loop {
let dividend = 100;
let divisor = get_number();
match divide_by(dividend, divisor) {
Ok(val) => println!("{dividend} / {divisor} = {val}"),
Err(error) => println!("Error {:?}", error),
};
}
}
fn divide_by(dividend: i32, divisor: i32) -> Result<i32, &'static str> {
if divisor == 0 {
return Err("Cannot divide by 0");
}
let res = dividend / divisor;
Ok(res)
}
fn get_number() -> i32 {
let mut number = String::new();
print!("Please type in a number: ");
io::stdout().flush().expect("Oups");
io::stdin()
.read_line(&mut number)
.expect("Failed to get input");
let number: i32 = number.trim().parse().expect("Could not convert to i32");
number
}
factorial function, runtime panic
-
panic!
-
In this series of examples we’ll see how a function we write can report error and how the caller can handle the error.
-
In the first example we implement a function to calculate factorial recursively. The specific task is not important, just that if the user supplies a negative number then this code will crash. More precisely, it will have a panic when our program reaches stack overflow.
fn main() {
for number in [5, 10, 0, -1, 3] {
let fact = factorial(number);
println!("{number}! is {fact}");
}
}
fn factorial(n: i64) -> i64 {
if n == 0 {
return 1;
}
n * factorial(n - 1)
}
Please type in a number: 5
5! is 120
Please type in a number: 10
10! is 3628800
Please type in a number: -1
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
./rust.sh: line 19: 13666 Aborted (core dumped) ./myexe $*
factorial create panic
In order to avoid this stack overflow our function needs to check the input and then if it is a negative number then report it. (I know, instead of i64 we could have used u64 which is an unsigned integer that would only allow non-negative numbers. but in other functions we might not be able to use the type-system for that. e.g. what if we expect a positive whole number larger than 2?
One way to avoid reaching stack overflow is to call panic! ourselves.
fn main() {
for number in [5, 10, 0, -1, 3] {
let fact = factorial(number);
println!("{number}! is {fact}");
}
}
fn factorial(n: i64) -> i64 {
if n < 0 {
panic!("Cannot compute factorial of negative number");
}
if n == 0 {
return 1;
}
n * factorial(n - 1)
}
Please type in a number: 10
10! is 3628800
Please type in a number: 0
0! is 1
Please type in a number: -1
thread 'main' panicked at 'Cannot compute factorial of negative number', examples/errors/factorial_create_panic.rs:14:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Out of bound for vectors
- Run-time panic
#[allow(clippy::useless_vec)]
fn main() {
let v = vec!["apple", "banana", "peach"];
println!("{}", v[0]);
println!("{}", v[5]);
}
apple
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 5', examples/errors/out_of_bounds_vector.rs:4:20
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Out of bound for arrays
- Compile-time panic
fn main() {
let v = ["apple", "banana", "peach"];
println!("{}", v[0]);
println!("{}", v[5]);
}
error: this operation will panic at runtime
--> examples/errors/out_of_bounds_array.rs:4:20
|
4 | println!("{}", v[5]);
| ^^^^ index out of bounds: the length is 3 but the index is 5
|
= note: `#[deny(unconditional_panic)]` on by default
error: aborting due to previous error
Open files
use std::fs::File;
fn main() {
let filename = "rust.json";
let res = File::open(filename);
println!("{:?}", res);
//let filename = "hello.txt";
//let res = File::open(filename);
//println!("{:?}", res);
match res {
Ok(file) => println!("{:?}", file),
Err(error) => println!("problem {:?}", error),
}
}
Error handling on the command line
- cmp
- Ordering
- std::cmp::Ordering
use std::cmp::Ordering;
use std::env;
use std::process::exit;
fn main() {
run();
}
// We are expecting the user to provide a command line argument.
// How do we handle if the user does not provide it (or provides too many)?
// In the get_name function call exit if the user did not supply the required parameter
// fn run() {
// let name = get_name();
// println!("{}", name);
// }
// fn get_name() -> String {
// let args: Vec<String> = env::args().collect();
// if args.len() != 2 {
// eprintln!("Usage: {} NAME", args[0]);
// exit(1);
// }
// args[1].clone()
// }
// Return the empty string
// This makes the caller need to handle the cases, but more problematic is that in the called
// we can't tell if there were to many or too few (or otherwise incorrect) parameters
// fn run() {
// let name = get_name();
// if name == "" {
// println!("Got empty string")
// } else {
// println!("{}", name);
// }
// }
// fn get_name() -> String {
// let args: Vec<String> = env::args().collect();
// if args.len() == 2 { args[1].clone() } else { String::new() }
// }
// Return either the Ok() with the value provided by the user or Err with the specific error message
// The caller needs to use match to sepearat the two cases.
// fn run() {
// let res = get_name();
// //println!("{:?}", res);
// match res {
// Err(error_message) => println!("{error_message}"),
// Ok(name) => println!("{name}"),
// }
// }
fn run() {
let res = get_name();
//println!("{:?}", res);
let name = match res {
Err(error_message) => {
eprintln!("{error_message}");
exit(1);
}
Ok(name) => name,
};
println!("{name}");
}
fn get_name() -> Result<String, String> {
let args: Vec<String> = env::args().collect();
match args.len().cmp(&2_usize) {
Ordering::Less => Err(String::from("Not enough parameters")),
Ordering::Greater => Err(String::from("Too many parameters")),
Ordering::Equal => Ok(args[1].clone()),
}
}
main returning Result - Set exit code
- Result
- Box
- dyn
- Error
- Ok
- ?
#![allow(unused_variables)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = std::env::args().collect::<Vec<_>>();
let filename = &args.get(1).ok_or("Missing file")?;
let number = args.get(2).ok_or("Param 2 was missing")?.parse::<u8>()?;
let num = number.checked_add(255).ok_or("overflow")?;
let content = std::fs::read_to_string(filename)?;
Ok(())
}
A number of invocations:
cargo run
cargo run data.txt
cargo run data.txt 10
cargo run data.txt abc
cargo run data.txt 0
Error in internal function
- Result
- Box
- dyn
- Error
- Ok
- ?
#![allow(unused_variables)]
fn main() {
let res = run();
match res {
Ok(_val) => println!("Ok"),
Err(err) => println!("Error: {err}"),
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
read_file("Cargo.toml")?;
//read_file("a.txt")?;
convert_number("23")?;
//convert_number("hello")?;
increment_number(0)?;
//increment_number(10)?;
Ok(())
}
fn read_file(filename: &str) -> Result<String, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(filename)?;
Ok(content)
}
fn convert_number(text: &str) -> Result<u8, Box<dyn std::error::Error>> {
let number = text.parse::<u8>()?;
Ok(number)
}
fn increment_number(number: u8) -> Result<u8, Box<dyn std::error::Error>> {
let num = number.checked_add(255).ok_or("overflow")?;
Ok(num)
}