Open simonsan opened 1 week ago
@simonsan Hey, sorry for the delay in replying, I have been meaning to write a blog post about this and was hoping I could get that out and just point at it, but I haven't even started and if I'm going to be honest, it's not going to get done very soon.
Anyway, my perspective on error handling has changed a little bit from when I wrote these docs and I should update them. Here's some notes:
anyhow
or something rather than designing a complicated error type.Result
(e.g., consider a parser in a compiler, it is good to separate the expected user input errors from real errors. Use Result
for the latter but not for the former). Adding domain-specific context, recovery in a parser, handling multiple errors, producing good error messages are all really hard to do within the constraints of Rust error handling, so don't even try. The only advantage is the control flow stuff, and although that feels nice at first, it is inevitably a bad trade-off in the long run.Some specific answers (all of which are very much 'IMO'):
When should we prefer returning a single error (e.g., Result<T, RusticError>) vs. returning a list of errors (e.g., Result<T, Vec
>)?
Always single error. If you have multiple errors, it is probably not a true error in the error handling sense of the term, but more just an expected error in user input which should be handled as part of the 'happy path'
In complex async operations or batch processing, where multiple errors might occur, what would be the best way to handle error accumulation without losing key context?
Basically avoid this at all costs. Handle the error close to where it occured so you don't need to propagate. Treat errors as a form of the regular output where appropriate. If it's a library crate, let the user handle this; API should just look like single async functions which might error in a simple way. If you've got complex concurrent futures stuff going on, that is a smell that the library is doing too much orchestration.
When handling warnings, would you recommend keeping them local (i.e., logging only) or propagating them back to the caller?
In an app process them locally or treat them as part of the 'happy path' code rather than an error. In a library, just the latter.
How would you handle situations where a function should continue executing but may want to indicate that warnings occurred (e.g., via an is_warn boolean flag or a list of warnings)?
Warnings should be accumulated somewhere and returned as part of the normal execution flow, not treated as errors.
In async tasks and concurrent operations, how do you typically manage error propagation and structured logging, especially when errors are collected from multiple spawned tasks? How can we ensure we get full visibility into errors without complicating error management?
This is very hard! Let me know if you figure it out :-) Especially for a library rather than an app.
We also thought about a nested Result where the outer Result can contain hard errors that lead to aborting the program.
I would avoid over-engineering your error types. Keep it simple and keep error types just for unexpected errors.
Again, this is just my PoV and it is a rather opinionated one (some would call it extreme). Reasonable people may disagree and the specifics of a project take priority over general principles, however, I think this is a good starting point.
I'll chime in to say that I generally agree with Nick's perspective here. Errors are for things that get passed up the call chain 80% of the time or more. Recoverability for errors is not common, and when it's needed you generally would create a small error type indicating the recoverable cases. The rest of the time, in a binary crate, just use anyhow
.
I maintain a crate called woah
which is intended to be an ergonomic version of Result<Result<T, LocalErr>, FatalErr>
as a single enum, but unfortunately the relevant trait, Try
, is not stable (and likely won't be stable soon), so while it's ergonomic on nightly Rust builds, it's not very easy to use on stable. You can use it on stable, but you can't apply the ?
operator to it.
Hi @nrc!
I’m reaching out to get some expert advice on the challenges we’re facing with error and warning handling in our Rust-based project,
rustic_core
. Our project is relatively complex, and we’re struggling to find the right balance between propagating errors (soft- and hard errors), handling warnings, and maintaining good user experience with clear error messages.Context of Our Problem
Error Handling:
Result<T, RusticError>
to propagate errors. Kind of a god enum approach, where we convert sub errors into that god error for handing it over at our API boundary. However, we often find ourselves in scenarios where multiple errors can occur (e.g., batch operations or validation processes of data collections), and handling only the first error results in lost context.Result<T, RusticError>
)Result<T, Vec<RusticError>>
)Result<Result<T, Vec<RusticSoftError>>, RusticHardError>
)Warning Handling:
is_warn
) to indicate if warnings occurred.General Pain Points:
Questions
Error Propagation:
Result<T, RusticError>
) vs. returning a list of errors (e.g.,Result<T, Vec<RusticError>>
)? Are there performance or architectural concerns that we should consider when deciding between these two approaches?Warnings:
is_warn
boolean flag or a list of warnings)? What is the best approach here to maintain simplicity while giving the caller enough control over decision-making?Async/Concurrency:
General Best Practices:
We appreciate any guidance or patterns you’ve found useful in these situations!