std::io::ErrorKind::NotFound error message: add filename

When you open a file that does not exist you get an appropriate NotFound error kind, however the message "No such file or directory" is missing the name of the file or directory that is missing. If your program is loading a lot of files it would be very helpful for the user to see which one of them is missing. Sure, one can always customize the error message and in a final product we should not unwrap() in such a case, but why not make the default user experience as best as possible?

fn main() {
    std::fs::File::open("doesnotexist.txt").unwrap();
}
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }

I propose to improve it as such:

called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "File or directory 'doesnotexist.txt' not found" }
1 Like

but why not make the default user experience as best as possible?

Because that would incur an allocation cost to carry the path, which has to be owned by the error message. io::Error is a slim representation of operating system errors, and those do not contain the path that failed. So if you want it to be userfriendly you have to enrich the error with some context, as various crates offer.

In other words, it's possible to add the data. But if it where there by default then everyone would pay the cost because it can't be removed.

Some kinds of filesystem access patterns encounter more error cases than success cases (e.g. probing a bunch of paths until a file is found), so making errors more expensive would affect them greatly.

See Include the errored file path (when available) in std::io::Error instances · Issue #2885 · rust-lang/rfcs · GitHub for some prior discussion.

7 Likes

(Slightly duplicate answer, I started writing this before @the8472 posted their reply :wink:)

One problem here could be overhead in error handling:

The std::io::Error type currently uses a compact representation (only 1 pointer large), which ensures it's cheap to pass around, but it also aids in minimizing the need for incurring any overhead in terms of allocations, in many common cases. For many functions, like File::open, this means the raw OS error code that the std::io::Error is created from is simply slightly bit-shifted and tagged, but otherwise left untouched. If the error message was supposed to mention the file name, with a type like std::io::Error: 'static, there would first need to be a new allocation created in order to remember this file name as part of the error.

3 Likes

Thank you for the GitHub link, I read through it and it was really interesting! It mentions crates.io: Rust Package Registry which I may use as a drop in for std::fs to get clearer error messages.

2 Likes

In the case of std::fs::File::open("doesnotexist.txt") the error type could share a lifetime with the argument though. Admittedly this would make it more difficult to just bubble it up the call stack.

1 Like

You'd have to .map_err() at lots of call sites to convert to an owned version, which makes it not that much easier to use than using .map_err() at each such site to add the path in either owned or borrowed form. Less error-prone, though (no chance of adding the wrong path).

I think in the vast majority of cases, the overhead of an allocation on error unimportant. It's a tiny optimization that most users won't notice, but it has very visible downsides that users are annoyed by.

(the size of the io::Error type is important since it shows up on every write, but open is relatively rare, and could afford using a slow path to build io::Error).

BTW, File::open already allocates a NUL-terminated and/or UTF-16 version of the path on the happy path. If you want the error to be super efficient, the implementation internals could recycle that allocation.

4 Likes

It uses a stackbuffer up to a limit (currently 384 bytes).