List of possible errors in Result<T, E>?

I'm trying to list all the files in a directory. So I looked up std::fs::read_dir, and it says:

This function will return an error if the provided path doesn't exist, if the process lacks permissions to view the contents or if the path points at a non-directory file.

Now I can destructure this into Ok() and Err(), but how do I know what concrete Err() I got thrown so I can handle it? The documentation doesn't have any list, nor does the type suggest anything.

Alright, maybe it's in the source. So I try to look up std::fs::read_dir, and see it's actually implemented in fs_imp::readdir (note the change in spelling), which is an alias for sys::fs, which I can't find in the online docs. Fine, grepping the source it is. So readdir() differs by os (do its errors? doesn't quite look like it, but how can I tell without reading everything?). Anyway, so it throws Error::last_os_error(). That's of course not in error.rs, but Error here is io::Error, and there I finally find out that it returns the OS's error number.

So to handle the error, I'll have to look up all error numbers any OS I want to support can throw, and check them, with (if I'm not mistaken) no help from match to show I conclusively caught all of them.

Is that right?

More importantly, is this usage of Result<T, E> normal, where the error is extremely vague and basically just means "stuff blew up, but at least your thread hasn't crashed"? Wouldn't you want to return precise error enums?

(And is this situation with std::fs typical, where I have to dig through several source files to figure out what it does? Or is std::fs just really barebones and under-documented? If so, what else should I use?)

The documentation for std::fs::read_dir does specifically show that it return io::Result<>, for which the Err part is Error in std::io - Rust. If you just follow the links (starting by clicking "Result" in std::fs::read_dir return type), this isn't entirely that hard to find. It's mostly a matter of knowing how to navigate rustdoc, which gets easier with time.

It is kind of confusing that io::Result<T> has the same name as Result<T, E>, but it really is just a hardcoded alias for Result<T, io::Error>. Whenever you see Result<> with just one type parameter instead of two, you can pretty much assume it's an io::Result instead of Result.

The kind() method on io::Error gives you an io::ErrorKind, which has all the variants listed. I'd assume it's pretty easy to tell which ones are which from their names: ErrorKind in std::io - Rust.

2 Likes

Thanks, that clears things up!

It hadn't occured to me to click the type names in the signature to figure out what they're aliased to. (Or that they're aliased.)

io::ErrorKind looks more useful. It seems pretty generic, in the sense that I still have to look up the underlying system calls (eg man readdir()) to figure out what kind of errors a function can concretely throw (or make a reasonable guess?), handle those ErrorKinds, and then just panic!() on the rest.

(Which, to be fair, is more readable and portable than handling the error numbers explicitly, but I was still hoping there'd be a reasonably convenient way to handle all possible errors of my limited system calls. But ErrorKind warns against exhaustively matching against it. Minor bummer.)

If you are sure there're match variants which will never occur, you can use unreachable!() macro, which panics with "unreachable" error message:

use ErrorKind::*; // so you don't have to write "ErrorKind::ThisOrThat" all the time
match err.kind {
  NotFound => create_that_file(),
  _ => unreachable!()
}

There's also a convention to make alias for result type in modules, which define some error, like type Result<T> = result::Result<T, mymod::Error>, as well as name module specific error types just Error. The io::Result and io::Error is just the most often case in std, but there're other modules with similar definitions, so don't be confused. E.g. another case is fmt::Result and fmt::Error.

2 Likes

ErrorKind has a hidden variant which isn't accessibly outside of stdlib to disallow from exhaustive matching. A _ branch in a match statement will allow you to handle another error which doesn't already have a match break though.

This is done mainly to allow for new generic error types to be added to ErrorKind without breaking existing compatibility with exhaustive matches. Because every match requires a _ case, any new variants added will already have something handling them in any existing match case.