Don't take the following too serious, I just want to point out that the "thou shalt not panic" paradigm also has downsides, particularly in the hands of less experienced developers like me.
It also impacts APIs since returning (meaningful) values for further use by an API becomes way more cumbersome.
As a Java-programmer, I get the checked exception feeling
Q1: Is there a universal way of wrapping conformant Rust code so you don't have to check every statement?
Q2: Does the former require the use of the ?-operator by the underlying code?
A small example:
use std::str::FromStr;
struct Demo {
vector: Vec<f64>
}
impl Demo {
fn new() -> Demo {
Demo{vector: Vec::new()}
}
fn add(&mut self, f64_string: &str) -> Result<(), Box<dyn std::error::Error>> {
let result = f64::from_str(f64_string)?;
self.vector.push(result);
Ok(())
}
fn add_with_panic(&mut self, f64_string: &str) -> usize {
match f64::from_str(f64_string) {
Ok(value) => {
self.vector.push(value);
}
Err(_e) => {
panic!("Invalid float: {f64_string}");
}
}
self.vector.len()
}
}
fn main() {
let mut a = Demo::new();
let _ = a.add("5.7");
// SILENTLY FAIL
let _ = a.add("pretty bad");
let n = a.add_with_panic("6.0");
println!("There are {n} elements in the vector");
let _ = a.add_with_panic("Not really a number");
}
Result:
There are 2 elements in the vector
thread 'main' (38473370) panicked at src/main.rs:24:17:
Invalid float: Not really a number
This is incorrect. add does not "silently fail" as your comment implies. By binding return value of a function to _ you are explicitly ignoring the returned value. In the context of function returning a result, like Demo::add, what you wrote means "I know that this function can error, but I choose to ignore it". There are many places where it is fine, but this one obviously isn't.
Note that if you don't bind returned value to _ and just call Demo::add ignoring returned value, Rust compiler will warn you about the fact that you ignore an error this function can return with following warning:
warning: unused `Result` that must be used
--> src/main.rs:34:5
|
34 | a.add("pretty bad");
| ^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
|
34 | let _ = a.add("pretty bad");
| +++++++
As the warning itself states using let _ = ... is a way of opting-out of this check.
What makes a reasonable precondition is both contextual and a matter of opinion.[1] The preconditions should definitely be documented. You can also offer both APIs.
Using ? to pass errors up the call stack with minimal ceremony is more idiomatic than always panicking or silently discarding errors, but also isn't great UX or bug-fixing-experience or library-consumer-experience if you don't take steps to add context.
For binary-like crates (where library-like consumers are less of a concern), crates like anyhow can ease some of the boilerplate of defining errors and adding context for such an approach. But in any case, great errors do take some effort and code; they're not free. (The UX really is markedly better though IMO.)
IMO, your method should just take a f64, but that could simply be a side effect of being a minimal example. ↩︎
Well, a coin has two sides. One is a side-effect; forcing methods to return a value even if you don't need one. If you have a error wrapper (Q1 in my posting), do you still get warnings?
That's a bad use of features. Features are meant to be additive, where any dependent can add the feature without beaking other crates. Changing the API breaks other crates.
When beginning Rust, to reduce the cognitive load (or some such thing) you can use panic and panic handling, it works, and is a little bit simpler (arguably).
But it means the panic abort is not an option, that is in your cargo.toml:
[profile.release] panic = 'abort'
I wouldn't like to say one way is better than the other, it depends on the situation. Broadly speaking though, you want to be able to use panic abort.
In a practical application I'm working with (CBOR encode/decode for security-related applications), there is more to this than cognitive overload. BTW, do Web servers stop to work if a panic is encountered? I guess not.
From what I can see, I'm not the only one struggling with Rust error-handling. Anyway, thank you everybody for looking into this posting and providing good feedback!
One using panic abort will. One using panic error handling may not, because it could be being used to handle not entirely-unexpected user input errors, but that is not the usual way to handle that. panic abort means a smaller binary ( well, that is what the documentation claims).