Wow, this is very cool, thanks so much for sharing! I've had this exact frustration with error types before and this seems like a really nice way to handle it. The ability to define errors as a small set of them that actually can occur at a given call instead of packing it into module-level enum with irrelevant errors is so nice for being able to refine what actually happens. I'm very excited to give this a go!
Another annoyance that I've run into for error handling is that it is annoying to write code that returns multiple errors. Say you're writing a backend CRUD application and you have to verify the fields of a create request. It would be nice to be able to return all fields that were in violation of the constraints rather than just the one, but it's difficult to do that with Result since ? returns early. I've seen some solutions using some sort of error vec crate, but they've never felt very elegant to me. I'm curious to see if some constructs in this crate have any hints at a solution.
exposing ParseIntError in this way feels iffy: this isn’t a failure to parse just any int, this is a failure to parse a port in particular. suppose your address contains two ints (maybe you’re connecting to a redis db and you have a port and a db number): now ParseIntError is ambiguous. this breaks either ? or automatic widening: now you need to specify which particular int failed to parse, by doing something like .map_err(ParsePortError) manually (just #[context] won’t do if you want to change this programmatically, because #[context] doesn’t change types).
for that reason, I think impls like From<ParseIntError> for ConnectionError are anti-patterns, and generally you should try to wrap “low-level” errors like these into more semantic types (or variants, you can have ConnectionError::ParsePort(ParseIntError) and ConnectionError::ParseDb(ParseIntError)). would be nice to see some infrastructure for that, although I’m not sure if it’s doable without some heavy macro use.
(a separate question is whether you should expose underlying errors at all in any particular case: maybe redis will transition to named dbs, and you’d wish you just left that error opaque)
I agree with your assertion. The example situation was not the best. It was more there to show the flow/api of eros. In real code don't expose ParseIntError unless the caller cares about that exact error type. The 1. philosophy in the README is listed as "Error types only matter when the caller cares about the type, otherwise this just hinders ergonomics and creates unnecessary noise."
so how would you approach exposing “port parsing failed” but not ParseIntError in programmatically-accessible way with eros? manual newtype + .map_err()?
I would've said a downside would be that you'd have to order your types in a certain way for e.g. .widen() and .narrow()... But it seems like it works regardless of ordering? The eros library seems to have a bunch of hacks tech to make your errors an actually unordered set.
Maybe compiler errors are a downside? I haven't tested it, but I'd suspect that if you happen to specify the same type twice / accidentally specify non-error-types / similar invalid combinations?
Regardless, I'd love to read a blog post about how all of this works.
I couldn't agree more with you that a post covering that logic would be great extra reading. From a cursory glance, it seems that this has a manual implementation for each count of different errors that is being returned via a eros::Result type? So, I would assume since this goes A-Z that would mean the limit is returning 26 distinct types of errors from a function. (Not that I could anticipate that bound ever being insufficient.)
Author here. Compiler errors can be a downside, since the type system is used pretty extensively here. But that really is only on the development front. For users, the api straight forward and the api will give you straightforward things like "the trait bound i32: SendSyncError is not satisfied"
OCaml has this approach to errors built-in thanks do its polymorphic variants feature. The downside in my experience is that when one can more easily make unions between sets of errors one will take that approach. Without that point where the programmer is given a moment to explicitly design the error it tends to end up under-designed, lacking any additional useful context that could have been added otherwise.
Author here. The only downside in my opinion is that is does not work on no_std without alloc. But even this I just figured out with the inspiration of a colleague. Hopefully a PR for this will be up in the coming days -- A lot of pieces to get right and conditional compilation, but the api will be the same. Overall for eros, I meticulously designed the api and am very happy with it. Full flexibility to the developer.
IIUC this is essentially anyhow with compile time type tags. The only thing I would worry about is how long it will take to compile, since the traits look a bit difficult to solve.
To my inexpert eye it looks like the subset search (the aforementioned unordered set trick) is quadratic to solve — but at least not exponential, and perhaps not a problem with a typical number of error types in the set? (I also see it’s arbitrarily limited to 26 error types, because the tuple generics have to be done explicitly.)
Well that mean incremental compilation is still fast. But look at the numbers, first time compilation is apparently taking 2-4x as long compared to anyhow. And I think if you want a more like-for-like comparison you should look at thiserror rather than anyhow. And I would also very much interested to see how the compile time grows w.r.t. number of error types in the union.
I really like error_set and seeing this is by the same author gives me some confidence!
It looks a lot like terrors that I'm curious about the differences.
And how does this fit in with error_set? Does it make the unions defined by error_set obsolete? But I guess it's still useful for writing the error structs...
I'd like to see a real project written with this stuff to pick up some patterns. I'll probably have a play at some point :)
This is super-cool and I'm very tempted to go try and apply this to a few projects. (But uh I have to do "real work" now.)
One possible downside I see: when you have to craft container error types or manually convert among them, you are kinda forced to think about context propagation. A bare "FileNotFound" error needs to be annotated from the caller to indicate which file was not found, or otherwise the message shown to the user will be near-useless. Merely being able to propagate io::Error doesn't carry this friction, so it'll be propagated without details.
Now, crafting container error types doesn't solve the problem above and it's still possible to end up with insufficiently-specified errors. But at least it offers a sort of checkpoint. @mcmah309, what is your experience with this?
If I understand correctly, this sounds like an issue regardless of using eros or not. You are saying io:Error does not carry all the information you need? In that case, wouldn't wrapping in a container type be enough -- e.g. FileNotFound(io::Error, String) (or even deconstructing the io:Error to whatever shape you want). This is what I would do if I had a recovery tool or message tool up the call stack that could do something based on that type. But if you just want to propagate the information, I wouldn't expose a plain io:Error if it is not actionable, but either way you could just add context .with_context(|| format!("Reading file {file_name}")). Depending on the recipient of this you could use .with_user_context instead. Another alternative is using the #[context] macro. See https://github.com/mcmah309/eros#context-macro and https://github.com/mcmah309/eros#context-placement-two-approaches
The source is here. It doesn't use Rust's union, but instead uses a TypeSet which in turn uses some type system magic to turn the set of errors into a compile-time linked list (of up to 26 elements).
abrambleninja | a day ago
Wow, this is very cool, thanks so much for sharing! I've had this exact frustration with error types before and this seems like a really nice way to handle it. The ability to define errors as a small set of them that actually can occur at a given call instead of packing it into module-level enum with irrelevant errors is so nice for being able to refine what actually happens. I'm very excited to give this a go!
Another annoyance that I've run into for error handling is that it is annoying to write code that returns multiple errors. Say you're writing a backend CRUD application and you have to verify the fields of a create request. It would be nice to be able to return all fields that were in violation of the constraints rather than just the one, but it's difficult to do that with
Resultsince ? returns early. I've seen some solutions using some sort of error vec crate, but they've never felt very elegant to me. I'm curious to see if some constructs in this crate have any hints at a solution.goldstein | 13 hours ago
exposing
ParseIntErrorin this way feels iffy: this isn’t a failure to parse just any int, this is a failure to parse a port in particular. suppose your address contains two ints (maybe you’re connecting to a redis db and you have a port and a db number): nowParseIntErroris ambiguous. this breaks either?or automatic widening: now you need to specify which particular int failed to parse, by doing something like.map_err(ParsePortError)manually (just#[context]won’t do if you want to change this programmatically, because#[context]doesn’t change types).for that reason, I think impls like
From<ParseIntError> for ConnectionErrorare anti-patterns, and generally you should try to wrap “low-level” errors like these into more semantic types (or variants, you can haveConnectionError::ParsePort(ParseIntError)andConnectionError::ParseDb(ParseIntError)). would be nice to see some infrastructure for that, although I’m not sure if it’s doable without some heavy macro use.(a separate question is whether you should expose underlying errors at all in any particular case: maybe redis will transition to named dbs, and you’d wish you just left that error opaque)
mcmah309 | 10 hours ago
I agree with your assertion. The example situation was not the best. It was more there to show the flow/api of eros. In real code don't expose
ParseIntErrorunless the caller cares about that exact error type. The1.philosophy in the README is listed as "Error types only matter when the caller cares about the type, otherwise this just hinders ergonomics and creates unnecessary noise."goldstein | 2 hours ago
so how would you approach exposing “port parsing failed” but not
ParseIntErrorin programmatically-accessible way with eros? manual newtype +.map_err()?wrs | a day ago
This looks fantastic! Is there a downside?
I'm vaguely wondering if there could be an
anyhowadapter so you could transform errors into this when you call existing code.madsmtm | 23 hours ago
I would've said a downside would be that you'd have to order your types in a certain way for e.g.
.widen()and.narrow()... But it seems like it works regardless of ordering? Theeroslibrary seems to have a bunch ofhackstech to make your errors an actually unordered set.Maybe compiler errors are a downside? I haven't tested it, but I'd suspect that if you happen to specify the same type twice / accidentally specify non-error-types / similar invalid combinations?
Regardless, I'd love to read a blog post about how all of this works.
siru | 11 hours ago
I couldn't agree more with you that a post covering that logic would be great extra reading. From a cursory glance, it seems that this has a manual implementation for each count of different errors that is being returned via a eros::Result type? So, I would assume since this goes A-Z that would mean the limit is returning 26 distinct types of errors from a function. (Not that I could anticipate that bound ever being insufficient.)
mcmah309 | 10 hours ago
Yes that is correct
mcmah309 | 10 hours ago
Author here. Compiler errors can be a downside, since the type system is used pretty extensively here. But that really is only on the development front. For users, the api straight forward and the api will give you straightforward things like "the trait bound
i32: SendSyncErroris not satisfied"lpil | 12 hours ago
OCaml has this approach to errors built-in thanks do its polymorphic variants feature. The downside in my experience is that when one can more easily make unions between sets of errors one will take that approach. Without that point where the programmer is given a moment to explicitly design the error it tends to end up under-designed, lacking any additional useful context that could have been added otherwise.
mcmah309 | 10 hours ago
Author here. The only downside in my opinion is that is does not work on
no_stdwithoutalloc. But even this I just figured out with the inspiration of a colleague. Hopefully a PR for this will be up in the coming days -- A lot of pieces to get right and conditional compilation, but the api will be the same. Overall for eros, I meticulously designed the api and am very happy with it. Full flexibility to the developer.Yes there is an anyhow adapter behind the
anyhowfeature flag https://github.com/mcmah309/eros#anyhowyshui | 22 hours ago
IIUC this is essentially anyhow with compile time type tags. The only thing I would worry about is how long it will take to compile, since the traits look a bit difficult to solve.
wrs | 21 hours ago
To my inexpert eye it looks like the subset search (the aforementioned unordered set trick) is quadratic to solve — but at least not exponential, and perhaps not a problem with a typical number of error types in the set? (I also see it’s arbitrarily limited to 26 error types, because the tuple generics have to be done explicitly.)
mcmah309 | 10 hours ago
Somone just benchmarked this. After the first compilation, the same as time as anyhow. https://github.com/dpc/eros-bench
yshui | 3 hours ago
Well that mean incremental compilation is still fast. But look at the numbers, first time compilation is apparently taking 2-4x as long compared to anyhow. And I think if you want a more like-for-like comparison you should look at thiserror rather than anyhow. And I would also very much interested to see how the compile time grows w.r.t. number of error types in the union.
reivilibre | 8 hours ago
I really like
error_setand seeing this is by the same author gives me some confidence!It looks a lot like
terrorsthat I'm curious about the differences.And how does this fit in with
error_set? Does it make the unions defined byerror_setobsolete? But I guess it's still useful for writing the error structs...I'd like to see a real project written with this stuff to pick up some patterns. I'll probably have a play at some point :)
jmmv | 7 hours ago
This is super-cool and I'm very tempted to go try and apply this to a few projects. (But uh I have to do "real work" now.)
One possible downside I see: when you have to craft container error types or manually convert among them, you are kinda forced to think about context propagation. A bare "FileNotFound" error needs to be annotated from the caller to indicate which file was not found, or otherwise the message shown to the user will be near-useless. Merely being able to propagate io::Error doesn't carry this friction, so it'll be propagated without details.
Now, crafting container error types doesn't solve the problem above and it's still possible to end up with insufficiently-specified errors. But at least it offers a sort of checkpoint. @mcmah309, what is your experience with this?
mcmah309 | 5 hours ago
If I understand correctly, this sounds like an issue regardless of using eros or not. You are saying io:Error does not carry all the information you need? In that case, wouldn't wrapping in a container type be enough -- e.g.
FileNotFound(io::Error, String)(or even deconstructing the io:Error to whatever shape you want). This is what I would do if I had a recovery tool or message tool up the call stack that could do something based on that type. But if you just want to propagate the information, I wouldn't expose a plain io:Error if it is not actionable, but either way you could just add context.with_context(|| format!("Reading file {file_name}")). Depending on the recipient of this you could use.with_user_contextinstead. Another alternative is using the#[context]macro. See https://github.com/mcmah309/eros#context-macro and https://github.com/mcmah309/eros#context-placement-two-approachesintarga | 4 hours ago
this is very similar to how error handling works in hare
alper | 8 hours ago
I was a bit confused whether this was an existing Rust feature or a "new invention".
Is it built using Rust unions? (Something I don't think I've ever seen used.)
It feels adjacent to Roc's open tag unions?
academician | 7 hours ago
The source is here. It doesn't use Rust's
union, but instead uses a TypeSet which in turn uses some type system magic to turn the set of errors into a compile-time linked list (of up to 26 elements).