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 = "2021"
# 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) }