4 ms·
Ok, that’s a valid point! Though there’s a trade-off there, right? If both bar and baz can not find a file, they’re both going to return their own FileNotFound
by jgilias 1y ago
Ok, that’s a valid point! Though there’s a trade-off there, right? If both bar and baz can not find a file, they’re both going to return their own FileNotFound error type. And then, if you care about handling files not being found somewhere up the stack, don’t you now have to care about two error types that both represent the same failure scenario?
- Groxx 1y agoif you have multiple files that are read in a function, and they might lead to different error handling... then sometimes yeah, perhaps they should be different types so you are guaranteed to know that this is a possibility, and can choose to do something specific when X happens. or to be informed if the library adds another file to the mix. it isn't always the case, of course, but it also isn't always NOT the case.
- dwattttt 1y agoA framing about the problem I don't often see is: when do you want to throw away information about an error case? Losing that information is sometimes the right thing to do (as you said, maybe you don't care about which file isn't found by a function, so you only use one error to represent the two times it can happen). Maybe it would make sense to consider the API a function is presenting when making errors for it; if an error is related to an implementation detail, maybe it doesn't belong in the public API. If an error does relate to the public function's purpose (FileNotFound for a function that reads config), then it has a place there.
- fwip 1y agoI agree, it's tough to know when the right time to toss information away is, to simplify things for the caller. When in doubt, I tend to prefer "wrapper style" errors for libraries, so the caller can match at whatever level of specificity they care about. As a toy example: match read_config() { Err(ConfigError(FileNotFound) => println!("No configuration file found. Supported config directories are: {}", get_config_dirs()), Err(ConfigError(IOError(filename, msg))) => println!("Access error for config file {}: {}", filename, msg), Err(ConfigError(ParseError(e))) => println!("Parse error: {:?}", e), Err(ConfigError(e)) => println!("configuration error: {:?}", e), Ok(config) => {...}, } The calling application could start with just the generic error message at the end, and over time decide to add more useful behavior for some specific cases. Of course, then the problem becomes "what are the useful or natural groupings" for the error messages.
- dwattttt 1y agoI did this with a moderate sized cli tool; it was really good to be able to see effectively every error state you could have at the top level of the program. Something I didn't find an ergonomic solution to though was pulling out common errors; I wanted to say, pull out any std::io errors that occurred at the top level, but all I found was to match and expand every error case to pull them out. I considered maybe a derive/trait based approach would work, but it was too big a hammer for that project.
- magicalhippo 1y agoI don't know Rust, but I really liked Boost.System's approach[1], which was included in C++11, and have used that scheme in other languages. The point there is the error is not just an error in isolation, but it has an attached error category as well. And the error categories can compare errors from other categories for equivalence. So for example, say you have an error which contains (status_404, http_result_category), then you can compare that instance with (no_such_file_or_directory, generic_category), and because http_result_category knows about generic_category, it can handle the comparison and say that these are equivalent[2]. This allows you to preserve the quite detailed errors while also using them to handle generic error conditions further up. That said, doing this for every function... sounds tedious, so perhaps not worth it. [1]: https://www.boost.org/doc/libs/1_88_0/libs/system/doc/html/system.html https://www.boost.org/doc/libs/1_88_0/libs/system/doc/html/s... [2]: https://www.boost.org/doc/libs/1_88_0/libs/system/doc/html/system.html#usage_testing_for_specific_error_conditions https://www.boost.org/doc/libs/1_88_0/libs/system/doc/html/s...
- im3w1l 1y agoTo me this is backwards. I don't think there is a common need to handle a generic file not found error. Let's say the user tries to open an image file. The image file exists, but when decoding the image file you need to open some config file which happens to be missing. That needs entirely different handling than if the image file itself was missing. Though, I suppose with something broader like IOException the situation is different. try { open file read some bytes read some more bytes } makes sense, as they all relate to the same underlying resource being in a good state or not.
- fwip 1y agoThe approach with the `error_set` crate lets you unify the error types how you like, in a declarative sort of way. If you want to always treat FileNotFound the same way up top, that's totally doable. If you want them wrapped with types/enums to reflect the stack they came up, that also works. The main page of the doc explains pretty well how things can be wrapped up: https://crates.io/crates/error_set https://crates.io/crates/error_set You define the data structure, and it'll take care of generating the From impls for you, so you can generally just do `f()?` when you don't care about the specifics, and `match f1()` and destructure when you do.