Or, "Why we should have std::fs::File::open_optional
".
This blog roughly covers the same problem domain as a blog from a sled database developer but has a somewhat different conclusion.
Error handling in Rust is a big topic, with a variety of opinions. This is even more true of the programming language design space in general.
The general 2021 Rust status quo
The Rust Book example on error handling has an example of a correct way to handle a common error path.
To save you the click, here's the code:
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => {
panic!("Problem opening the file: {:?}", other_error)
}
},
}
There are popular crates like thiserror and anyhow.
A number of crates and APIs don't strongly differentiate likely-fatal versus likely-not-fatal errors
The above "file not found" case is in my experience by far the best example. It's true in most programming languages; e.g. in Python open()
will throw an exception you need to catch, Go has os.IsNotExist()
etc.
I would say that in my experience, 99% of the time I am matching on a filesystem error it is to handle "file not found".
And particularly in Rust, we have a nice Option<T>
type that matches the domain here perfectly. So I'm arguing that the standard library should have this:
diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs
index 014d5119b05..f9b3e69265e 100644
--- a/library/std/src/fs.rs
+++ b/library/std/src/fs.rs
@@ -328,6 +328,33 @@ pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
OpenOptions::new().read(true).open(path.as_ref())
}
+ /// Attempts to open a file that may not exist in read-only mode.
+ ///
+ /// See the [`OpenOptions::open`] method for more details.
+ ///
+ /// # Errors
+ ///
+ /// This function will return `Ok(None)` if the file does not exist.
+ /// Other errors may also be returned according to [`OpenOptions::open`].
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// use std::fs::File;
+ ///
+ /// fn main() -> std::io::Result<()> {
+ /// let mut f = File::open_optional("foo.txt")?;
+ /// Ok(())
+ /// }
+ /// ```
+ pub fn open_optional<P: AsRef<Path>>(path: P) -> io::Result<Option<File>> {
+ match OpenOptions::new().read(true).open(path.as_ref()) {
+ Ok(f) => Ok(Some(f)),
+ Err(e) if error.kinds() == std::io::ErrorKind::NotFound => Ok(None),
+ Err(e) => Err(e),
+ }
+ }
+
/// Opens a file in write-only mode.
///
/// This function will create a file if it does not exist,
While this is obviously just a minor convenience, the standard library is also the "baseline" around which the larger Rust ecosystem evolves. It's also about reflecting best practices there.
In other words, this blog post isn't just about files. The core argument here is:
If any regular user of your API is likely to match on an error type, consider
whether instead the pattern of using e.g. anyhow::Result<SomeOtherType>
would
work better.
The sled developer above argues for nesting Result<T>
, which to me is ugly
and also it's still easy to "double unwrap" accidentally. Specifically, I would
argue that a better API for that case in sled is:
fn compare_and_swap(
&mut self,
key: Key,
old_value: Value,
new_value: Value
) -> Result<Option<CompareAndSwapError>, sled::Error>
It is a bit ugly in that the "happy path" is None
, but again I think that's better than also unintentially supporting tree.compare_and_swap(...)??
. The point here is, the callers of this API should generally be inspecting the inner value and not using ?
on it.
But I'm writing a foundational crate!
Right, for something like e.g. hyper
, the custom hyper::Error type makes complete sense.
I'm not arguing against crates like thiserror. I'm arguing that one shouldn't blindy think "this is a library, I should use thiserror for everything".
Even in "foundational" library crates, there are still often APIs for which callers aren't ever going to match on.
Conclusion
Let's normalize using anyhow::Error
in library crates for "should be fatal" errors.