I was hoping someone might be able to suggest a good case study of someone having elegantly managed to wrap std::io::Error
inside of an Error
struct that implements serde::{Serialize, Deserialize}
traits? It's popped up in threads a couple times (such as here and here in the serde
issue tracker on Github), but I haven't been able to find the actual in-code examples of folks having done so successfully. Any suggestions would be greatly appreciated. Thanks!
You can't implement foreign traits (Serialize, Deserialize) for a foreign type (std::io:Error) due to Rust's orphan rule. What you can do instead is create a newtype and implement the traits for it.
In one of the links that you shared there's an example for this.
std::io::Error
does expose quite a few of its structural details; admitted ErrorKind
being non-exhaustive is a slight limitation, and the ability to wrap any Box<dyn Error + Send + Sync>
cause means you’ll have to lose some details there… what information do you require here (w.r.t. Display
, Debug
and .source()
info) – perhaps for certain concrete types, you’re even interested in preserving the actual value, as obtained via downcast
? So there’s some customization to be had.
Also, in cases where it wraps a RawOsError
, you can inspect and represent that if you like, but it would not preserve meaning across platforms, so there’s a design decision to be made, perhaps specific to the intended use-case.
Yes, on the same page about the orphan rule! I believe this tends to be taken into account by most error types via
enum Error {
Io(std::io::Error),
Other
}
which sort of natively implements the NewType pattern as a wrapper anyway, correct? Are you referring to the first link #2268, where someone says the following?
impl Serialize for PrintError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer {
serializer.serialize_str(self.to_string().as_ref())
}
}
I can see that in this case Serialize
creates a String
error message, but misses stuff like ErrorKind
, in addition of course to a potential reference implementation of Deserialize
as well.
Definitely agree on some of the challenges there! I think I was largely hoping propagate the ErrorKind
and a String
of the error message, if it exists. I'm using thiserror
to wrap the error, so I think that I should be able to take care of things like Display
via the #[derive(Error)]
macro which helps with that.
I guess it just feels to me like I can't be the first person to be trying to implement serde
for something with io::Error
, and I'm sure that some other libraries must have tried to implement something similar at some point. To your point, due to the variance, it's unlikely that there's a single idiomatic pattern, but was hoping to find maybe a couple of examples that I could base an implementation off of
Edit: I also don't really mind tossing in an _ => Other
statement as well to deal with non-exhaustive; this is largely best-effort, and I have a number of other error modes that I'm trying to manage, and would love to get something mildly elegant outside of just skipping that field altogether in my larger Error
enum.
As a starting point, it looks like you can think of io::Error
internals as essentially an enum
:
// the type *synonym* is unstable; the numerical value of the error code,
// as an `i32`, is stably accessible though
#[unstable(feature = "raw_os_error_ty", issue = "107792")]
pub type RawOsError = i32;
enum Error {
Os(RawOsError),
Simple(io::ErrorKind),
SimpleMessage(&'static (io::ErrorKind, &'static str)),
Custom(Box<(io::ErrorKind, Box<dyn std::error::Error + Send + Sync>)>),
}
Of these cases, Os
, Simple
and Custom
are all user-constructible, via io::Error::from_raw_os_error
, <io::Error as From<io::ErrorKind>>::from
and io::Error::new
, respectively.
And you can detect the Os
case with io::Error::raw_os_error
getting the RawOsError
, detect the Custom
case with io::Error::get_ref
, accessing the dyn Error + Send + Sync
, while the ErrorKind
is accessible with io::Error::kind
in this case.
Serializing the dyn Error
, and deserializing the result into something sensible is its own design challenge, as noted above, though maybe serde_error - Rust could be a reasonable approach.
Constructing SimpleMessage
appears impossible (relevant to Deserialize
), but a Custom
with a Box<String>
seems like an appropriate approximation to me. If you care about deserialize>>serialize to re-produce the thing you’ve started with, then you could consider adding String
as a special-case (handled by trying a downcast
) for dyn Error
serialization, and give a String
-containing Custom
io::Error
the same serialized representation as SimpleMessage
.
SimpleMessage
and Simple
are a bit tricky to differentiate (without relying on technically-unstable Debug
output). The most practical approach is probably to compare the Display
-output of the io::Error
with the Display
-output of the contained io::ErrorKind
, and just assume Simple
whenever they’re equal. (Though caveats about ErrorKind
’s unstable variants apply; see below in the last paragraph. This means, if you’re going to convert an unstable variant to ErrorKind::Other
, then preserving its original Display
output might be desirable even when the original error was of the Simple
kind.)
Display
is also the way to extract the contained message (e.g. as a String
) for the SimpleMessage
case; and io::Error::kind
– once again – provides the io::ErrorKind
for the SimpleMessage
case or the Simple
case.
io::ErrorKind
can be worked with by pattern-matching; e.g. translating between it and an identically-defined enum
of your own which derives Serialize, Deserialize
; while non-exhaustiveness can be handled by just mapping possible future extensions (as well as existing unstable kinds, and the hidden unstable Uncategorized
case) to Other
.
Yes. How much information you want in the error's json representation will depend on how far down the rabbit hole are you willing to go.
This is really comprehensive, and I appreciate the time you put into this response In all candor, it sounds like I probably have some experimenting to do! I'll probably start with building a roughly-equivalent enum that I can derive those traits on, then start trying to work my way down a pattern-matching tree for
From
/Into
for bits and pieces to enable handling the relevant cases. If I can come up with a pattern that I think works well in the near-ish future, I'll tag an update onto this thread. Thank you again!
If you're creating your own Error
type, then instead of storing io::Error
as-is, I suggest writing your own impl From<io::Error> for Error
and extracting all the information you want from it right there.
The io::Error
can be arbitrarily complex due to holding Box<dyn Error>
and potentially a whole chain of source()
errors of various types, so if there's any information in there that you need, get it and keep it while you have the original object available, instead of trying to make it survive serialization and deserialization.
Thank you for the suggestion! Just to clarify, this would also then mean that each time I had a section of code in which I could come across an std::io::Error
, you're suggesting it would look something like the following
fn do_some_io() -> Result<(), MyError> {
potentially_creates_io_error().map_err(|e| Into::<MyError>::into(e))?
Ok(())
}
#[derive(Serialize, Deserialize)]
enum MyError {
// This
Io(String),
// Not this, which would toss an error for those traits
BadIoPattern(#[from] std::io::Error),
Other
}
impl Into<MyError> for std::io::Error {
fn into(value: std::io::Error) -> MyError {
// Extract the information from io::Error
// For example, `match`-ing on ErrorKind and turning that into a string
let msg = get_string_from_errorkind(&value);
MyError::Io(msg)
}
}
Normally you implement From not Into:
https://doc.rust-lang.org/std/convert/trait.From.html
One should always prefer implementing
From
overInto
because implementingFrom
automatically provides one with an implementation ofInto
thanks to the blanket implementation in the standard library.
You don't need to call the into
method since the ?
operator calls from
for you.
fn do_some_io() -> Result<(), MyError> {
potentially_creates_io_error()?; // <<<< calls `from`
Ok(())
}
impl From<std::io::Error> for MyError {
fn from(value: std::io::Error) -> MyError {
// Extract the information from io::Error
// For example, `match`-ing on ErrorKind and turning that into a string
let msg = get_string_from_errorkind(&value);
MyError::Io(msg)
}
}
?
calls From
, so you don't need to change any syntax at the call site.
#[from]
in thiserror
is just boilerplate for the most basic impl From
code, nothing magic. You can write your own.
So the question is, what information we want from io::Error
.
I used to capture all kinds of errors and convert them into my crate errors via ?
by wrapping them. Until I wanted my crate error to be Clone
, but the problem is that not all of the wrapped errors are Clone
.
Then I thought, what do I need from an error anyway? Usually, just the message, and also all messages from sources. So instead of wrapping, I now rather convert all errors to my crate error by taking the message, and the messages recursively for all sources, and possible add a prefix to the message indicating the error type.
Also, for io::Error
, if it comes from a file access, as it most often does, I wrap it in a crate error that captures the file name.
So every error becomes a nesting of only crate errors. I have not yet looked into serializing and deserializing errors, but basically, they are just a list of Strings.