Unclear on how to use multiple error types
The snafu::guide::philosophy says that
Each module should have one (or more!) error types that are scoped to that module, reducing the need to deal with unrelated errors when matching and increasing cohesiveness of a single error type.
But doesn't really say how to best go about this in a project. The example uses just one error type.
In particular, I'm not clear on whether or not to define a top-level error to which all the module-specific ones convert and along with it a Result type ... Or whether to not have one crate-wide Result type and convert between various Results.
What's the recommended approach? Does anyone know of a project that implements this well I could have a look at?
The example uses just one error type.
The curse of examples, I suppose: how to make them small enough to understand quickly but large enough to provide value.
Pragmatically, many people like to have just one primary error type. You can accomplish this using pub(crate):
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))] // Default for this error
enum Error {
OpenConfig { source: io::Error },
SaveConfig { source: io::Error },
}
If you put that at your crate root, you can then do something().context(crate::OpenConfig) anywhere (or import it directly, etc.)
The stye I prefer and advocate for tries to group errors into categories, often based on the existing module structure. This does assume a specific kind of usage of modules; if you don't use modules in that way, it's fine:
#[derive(Debug, Snafu)]
enum Error {
UploadFile { source: config::Error, image: path::PathBuf, server: String },
// ...
}
mod config {
#[derive(Debug, Snafu)]
enum Error {
LoadCredentials { source: io::Error, path: path::PathBuf },
// ...
}
}
I roughly equate each error type to a semantic level in a backtrace. When something wrong happens in a program, I think it's a useful exercise to explain as a human what it was:
- I couldn't upload the image I to the server S because
- I couldn't load the username and password from the config file C because
- I couldn't find the file
To me, this maps well to the nested nature of the errors as well as context fields.
I would create helper Result types for each created error type though, just for usability.
#78 is relevant as it will reduce some of the annoyance of adding more layers. With the example above, if the only time we used config::Error was via UploadFile, we'd still have to say config::load_credentials().context(UploadFile)?. #78 would allow just config::load_credentials()?.
@shepmaster Thanks a lot for the detailed answer. As a matter of fact, I ended up iterating to pretty much the same conclusion as you described - having a snafu Error for each semantic level of an error message. I'd been worried there'd need to be a lot of boilerplate for conversion between the various errors, but in fact the whole thing seems to have come together rather nicely.
FWIW, I think the quick example is just fine, I don't think it would be wise to expand it further. I was thinking of there being another example specifically for the multiple errors scenario.
I should not promise anything, but I'd like to put together such example, I suppose it could also be a way to get feedback on the design...
In any case, thanks for a great error management library!
@shepmaster I've written a short-ish multiple error types example: https://gist.github.com/vojtechkral/348422c487954faefb1e9f1b2984e0be If you're interested, let me know what you think...