Rust, both by design and by convention, has a fairly strongly defined model for strict error handling, designed to force developers to deal with errors up front rather than assume they don’t exist. If you stick to a few conventions and principles for best practice, error handling becomes fairly straight-forward (although what you ultimately do with the errors is a different question) if you are living in an all-rust world. The problems start when you step foot outside of the comfortable world of crates and Result
s, such as when dealing with FFI to interface with C libraries or using rust in an embedded context.
The typical approach for dealing with errors in rust in 2018 is to have any function that can encounter a scenario wherein it is unable to return a valid value declare a return type of Result<T, E>
where T
is the expected result type in normal cases and E
is a type covering possible errors that can arise during execution. When calling from one such function into other functions with non-guaranteed success, the typical control flow in the event of an error is almost always “early out”:
/// A function that does something and produces no result,
/// except in case of an error, in which case it returns an
/// instance of the type `Error`
fn do_something() -> Result<(), Error> {
inner_func_call();
inner_func_call_may_fail()?;
return Ok(());
}
In the example above, the ?
in inner_func_call_may_fail()?
is shorthand for “if the result of inner_func_may_fail()
is an error of type Error
, bubble it up to the caller immediately rather than continuing on to the next line in this function (and if it produced a result, “unwrap” the Ok(whatever)
result to evaluate to whatever
directly).
So far, so good. I’m not going to delve into the nitty-gritty of match
blocks, what an Error
type declaration looks like, or what you ultimately do with the error when it can bubble up no more. But let’s look at a common practice from the C world:
int destroy_resource(Foo *foo) {
if (_lucky) {
return STATUS_SUCCESS;
}
return STATUS_SOME_ERROR;
}
int do_something() {
Foo *foo = create_resource();
if (!foo) {
return STATUS_CREATION_FAILURE;
}
int result = use_foo(foo);
if (result != STATUS_SUCCESS) {
destroy_resource(foo);
// bubble up the original `result`
return result;
}
// do some other stuff
result = destroy_resource(foo);
if (result != STATUS_SUCCESS) {
return result;
}
return STATUS_SUCCESS;
}
Pay close attention to what happens in case do_something()
failed to execute use_foo()
successfully: it runs a cleanup function that may fail but bubbles up the original error result
rather than the error returned by destroy_resource()
– which makes sense, since it’s the original error that started this problem in the first place. In fact, in this particular case we don’t even handle the result of destroy_resource()
at all, mainly because there’s nothing we can do about it. It was a “best effort” attempt at freeing allocated resources – we called the function for its side effects, but we don’t rely on the result of the function for anything, i.e. we are not predicating future actions based on assumption that this call succeeded.
In rust, in developing an abstraction for this C API (so you still need to worry about things like resource allocation and memory leaks since they haven’t yet been abstracted away), you might be tempted to write it as follows:
fn do_something() -> Result<(), E> {
let foo = create_foo()?;
let result = use_foo(&foo);
if result.is_err() {
free_foo(foo)?;
return result;
}
// do other stuff
free_foo(foo)?;
return Ok(());
}
Or if you’re a masochist that insists on using match
exclusively and shuns return ...;
at the end of function blocks “because clippy said so”, you might write it like this instead:
fn do_something() -> Result<(), Error> {
let foo = create_foo()?;
match use_foo(&foo) {
Ok(_) => {},
Err(e) => {
free_foo(foo)?;
return Err(e);
}
}
// do other stuff
free_foo(foo)?;
Ok(())
}
The point is, the error handling above in both cases is likely wrong, because the error that gets bubbled up here is the result of free_foo()
, which went wrong only after use_foo()
failed. The result
of use_foo()
contains the real Err(e: Error)
we are interested in, that is what should have been bubbled up to the caller instead. If you were to instead omit the ?
from that call to free_foo()
, the correct error (result
) would be bubbled up to the caller — but the compiler will complain about an used result:
warning: unused result which must be used
A warning that comes from a good place, but is nonetheless complaining about a purposely ignored return value (there’s nothing we can do about it here and there’s a more correct Error
to be bubbled up).
Since there’s literally nothing we can do about an inability to free the Foo
instance, if we were to handle the result to try and silence the warning, the code would necessarily look something like this:
fn do_something() -> Result<(), Error> {
let foo = create_foo()?;
let result = use_foo(&foo);
if result.is_err() {
match free_foo(foo) {
_ => {}
};
return result;
}
// do other stuff
free_foo(foo)?;
return Ok(());
}
With an empty match
block that serves no purpose but to silence the compiler (and gets optimized away into nothing). A beginner to rust might be tempted to use free_foo(foo).unwrap()
instead to work around that warning, but that would be a grave mistake since the function might very well fail, we just don’t care if it does — but calling .unwrap()
on a Result
means incurring a panic if it indeed evaluated to an error.
ignore-result
is a very simple (no_std
) crate that extends Result<_, _>
with a .ignore() -> ()
method that consumes a Result<T,E>
coming out of a function and always returns ()
– regardless of what the T
and E
types were. This represents a deliberate handling of both the Ok
and Err
variants on your end (quieting the warnings), discards an error we can’t do anything about while simultaneously blocking us from assuming success by return ()
regardless of what T
was in this case. In calling .ignore()
, you are explicitly saying “I don’t care of this function succeeds or fails, but I need to call it anyway (and I’ll pray it succeeds but no sweat off my back if it doesn’t),” which is perfectly safe since you can’t violate that contract with the code/compiler by then going ahead and using the Ok(whatever)
variant (like a call to .unwrap()
would return).
Compare the resulting code to the variants above, and I think you’ll agree it’s simpler, shorter, more expressive, and nicer to boot:
fn do_something() -> Result<(), E> {
let foo = create_foo()?;
let result = use_foo(&foo);
if result.is_err() {
free_foo(foo).ignore();
return result;
}
// do other stuff
free_foo(foo)?;
return Ok(());
}
I don’t expect that typical rust developers coding within the rust ecosystem will have need of this crate, it’s expressly for use by developers interacting with non-rusty APIs. It doesn’t do anything magic or anything special, but it does make your code (and the intend behind it) much clearer and more succinct than an empty match
block ever could. The crate is no_std
with zero dependencies (the IgnoreResult
trait was yanked from some embedded rust code for a rust-powered RFM69HCW library) and ideally compiles away to nothing at all.
As the Ignore
name is already in use by another (wonderful) library, this crate can be found at crates.io under the name ignore-result
and is released as an open source (MIT-licensed) library to our github profile. The docs are available on docs.rs.
Mahmoud Al-Qudsi.
I like your effort to find out if your customers are happy with your products.
I had a situation where one ssd that three partitions, two of which were empty and I had no idea why there were two very small partitions. I removed the partitions. Evidently there was some kbs of program that I interfered with the function of a program. When all partitions were removed, my SSD worked very well. You might use a statement where a ssd dedicated to windows, some how has an empty partition it must be removed, otherwise it is the first that is listed and thus the program will not function at all. . Once removed there were no problems. I actually have two win 7 ssd s. The second a Kensington I bought from Amazon at $14 and the adapter for my Sata tower.
This way I can boot to on or the other ssds and continue my research. I also have four 2t Sata drives in the tower.
If you email me at gonzal13@roadsrunner.com I shall add you to my contacts.
free_foo(foo).ignore();
can also be written as
let _ = free_foo(foo);
without the need of a crate.
@j that is the approach we have actually been using and now recommend. 👍