Rust error handling
bitfieldconsulting.com> But, since good programs don’t panic, and neither do good programmers, it’s very rare that using unwrap or expect is actually the right thing to do.
I respectfully disagree.
The assert!() macro is a way to document invariants. It's a bug if a variant is violated. It shouldn't have happened, but if it happens, then there's nothing the user can do except reporting the crash.
The unwrap() and expect() method document the invariant that the None and Err() variants shouldn't occur.
It's fail fast.
You should use error handling only if users would be able to handle the errors.
Tell the end user that the file they wanted to open is not readable, for example. Tell the users of your library that an error happened, like a parse error of a config file. And so on. Tell the users what they can fix themselves.
A bug in your library or program, that's something different. Fail fast! And Rust panics are perfect for that.
The easiest counterpoint to this is to think about a HTTP request. If your library is called by one route, and is known to fail in certain circumstances, this failure should not bring down the entire system. A well designed library should generally not make the choice to crash a system - that's the caller's decision. Yes there are exceptions to this, which is why TFA stated this as "rare" not "never".
Using the assert macro in your code is (in my experience) generally bad. If your code is written well, you can never test that code path. Document invariants with tests instead, or better yet with infallible code.
I think you’re misunderstanding the “fail fast!” advice. You want to fail fast in development not in production. In production you want maximum robustness. Users would rather see an error saying “whoopsie, maybe try that again later” than for the program to exit. That’s part of the reason why the functional error handling patterns are becoming so popular these days. They force you to handle errors and give you type-level info about how the program can fail.
I want my programs to fail fast in production too, because it makes it less likely that even bigger problems will arise. There are many problems that are much worse than a program crashing.
It depends on the program. You don't want your whole web server to crash because of a small error in one route. You probably don't want your CAD program to instantly crash and dump an error to the console because of a divide by zero in the constraints solver.
Though in some cases like that I think it might be appropriate to use `catch_unwind()`.
If you're writing something safety critical like avionics flight control software, you probably don't want to crash in production either. I've also always interpreted "fail fast" as "Make defects obvious during development so they don't exist when you deploy to customers."
These are thought provoking counter examples.
So, on a micro-level, let's say I have a function that expects x to be a float between 0 and 1, and there's some math and logic built on this assumption. Of course, in development if this expectation is violated, we fail fast and loudly and then fix the problem. In production it's not quite so clear. But still, is it ever the right thing to ignore that an invariant is violated and just hope that things somehow work out?
> Make defects obvious during development so they don't exist when you deploy to customers.
I agree with this phrasing. From another viewpoint, I would say:
If your code checks an invariant, and that invariant is broken never (never!, not even in production) just continue on and hope that things will work out.
> But still, is it ever the right thing to ignore that an invariant is violated and just hope that things somehow work out?
Yes! Of course! In many situations it probably doesn't matter that the numbers go a little wrong, and that result will be better than crashing. In other situations it will be better to crash than to give junk results. As we already said, it depends on the situation.
You can catch panics.
I'm not sure that is something you can rely in general due to panic = abort, though to be fair that's more a concern for library developers since IIRC they don't control that particular setting.
Panics are unexpected and not the preferred mechanism of error handling.
Stick to Result<T,E>.
The GP said
> Users would rather see an error saying “whoopsie, maybe try that again later” than for the program to exit
To achieve this, you need to catch panics. Rust does not use Result for things the OP is talking about like asserts.
If you want to handle the assert cases, you should not use the asserts in the first place.
That isn't possible. The assert may not even be produced in code you control.
The reason panics/exceptions exist is it is too onerous to handle every possible error condition at all callsites (allocation failure and broken pipes are the famous examples), and it is not possible to enumerate all possible error conditions (unintentional programmer errors for example).
People have religious ideas about handling panics for some reason.
Panics shouldn't exist in modern Rust (if there is such a thing) at all.
I take it you mean panics should always abort?
Maybe not perfect, but it seems to work out better than exceptions. Exceptions are a good idea which turned out to be too complicated.
A language has to use destructors to clean up for almost everything for this to work. "?" has no "catch" clause within the function. So if an object has an invariant, and that invariant must be restored on error, the destructors must restore the invariant. If that just means unlocking locks, closing files, or deleting data structures, that works. If some more complex invariant needs to be restored, "?" isn't enough. The calling function has to clean things up. This usually means encapsulating the function that does the work in a function that handles errors and cleanup. Basically, a try block.
This recent post resonated with me: https://cedardb.com/blog/exceptions_vs_errors/
There are certain obvious (and some less obvious) benefits to both exceptions and results, but I get the impression a lot of programmers have overreacted against exceptions.
Exceptions "just work" the same in every codebase and require little boilerplate in most languages. I think results really shine for internal business logic where errors are more "invalid" than "exceptional."
That's what panics are for. They compile to the same code c++ exceptions would. You can unwrap your results to turn them into exceptions.
"Go’s Error Handling Is Perfect, Actually" [1]
https://blog.verygoodsoftwarenotvirus.ru/posts/errors-in-go/
> Spend any amount of time in programming circles, and just as the sun rises and falls, you are certain to hear someone complain about error handling in Go. These complaints are, anecdotally, rarely well thought out suggestions on what error handling could or should be like in a language like Go, but often merely boil down to “I don’t like having to look at it”.
I read it. The first paragraph dismisses preferences in a matter that boils down to preference.
> Note that for any sufficiently complex program that invokes many dependencies, this stack trace will be so far down the chain that you may not even see where you’re making the call that causes it.
I don't understand this part. Why would my code not appear in the stack trace? Did this author know how to read stack traces?
Personally I find it much faster to pinpoint errors in the Go style.
With a stack trace, I have to cross-reference with code (ensuring versions match) and filter out a bunch of irrelevant calls in the stack. It’s not uncommon for the stack trace to end deep in library code with the root cause being many calls removed, making me check through a bunch of call sites to figure out what happened.
In Go if good context is added to errors, an error log is generally enough on its own to make it obvious exactly what went wrong.
Adding good context is also possible with exceptions.
I think people often compare good Go code with, say, bad Python code where every good practice has been ignored. Go is new and people who write Go are more likely to be skilled and enthusiastic about its design philosophy, so it is somewhat true that Go code is more likely to be high quality.
There's nothing Go error handling does that cannot be done with exceptions. When it comes to bad code, bad Go code is likely to ignore errors or have no context, whereas bad Python code is less likely to ignore errors and at least a raw stacktrace has some context.
This is an argument as old as time though.
This is a very long post to say "Go's Error Handling would be better if it was like Rust's, but the language designers made a mistake early-on and now we don't want to fix it"
I understand that writer's position that they can't go back and fix it now, but in my mind "We can't very well fix it now" is quite different from perfect. I think such a rationale justifies every choice as equally "perfect" and is thus useless.
Except for anything more production quality, one needs to lean on third party crates to compose errors without explicitly write tons of boilerplate composing result types.
Something that should be supported directly.
I agree that it's not ideal, but using something like anyhow/l and thiserror honestly doesn't feel _that_ bad.
> Something that should be supported directly.
Rust has "adapted" some crates into stdlib in the past. Are there any efforts to that for error handling?
We're on the like, third (fourth?) generation of error handling crates, and while there's some degree of consensus happening, I'm not sure that it's time for it yet.
If Rust had adopted error_chain into the stdlib, that would have been a huge mistake.
So far only the related traits.
That is the kind of excuse we give in C and C++ land, using static analysis isn't that bad.
For those who want to experiment with this style in C#, I've found this package to work: https://github.com/JohannesMoersch/Functional
Another good option I’ve personally used if you want a smaller API surface with just Result and Maybe concepts is True Myth. https://true-myth.github.io/true-myth-csharp/
Isn't it based on ML family? I mean, I see Rust error handling heavy inspired in monads used in languages like OCaml and Haskell.
Is Rust doing something different?
Rust's error handling isn't at all like using a monad. The entire point of being able to express the monad for something like the behavior you expect from error handling is that you write code which automatically propagates the error and in the same breadth prevents you from ever being able to see the error. The result is essentially exactly exceptions: you program the happy path and it entirely hides errors from you, as that's the point of the monad, which for this purpose you can pretty much conceptualize as programmatic flow control (or, even more crudely, an overloaded semicolon / statement separator operator).
Rust is using the data structure, but doesn't have a way to express or use the monad, so you have to deal with and manually propagate the error. But like, if you do not have the monad, the data structure is just awkward... the only reason the data structure for this monad even exists at all is to support the monad, and the reason for the monad is to get a syntax similar to exceptions! In a language with a ton of hard-coded syntax for all of these things you'd use a monad for in Haskell--whether it's error handling, asynchronous execution, scope allocation... whatever floats your boat--you should just use exceptions.
You can choose to work with Rust's Result in a monadic way, that's what methods like Result::and_then and Result::or_else and so on are for.
Because it's just another type you could also do whatever else you like, unlike with the Exceptions in typical languages which have them where too bad, we bolted the information to the control flow so now we're going on a journey.
If you want to bolt control flow to some information in Rust that's fine, feel free to define a function which returns ControlFlow::Break for success if that suits you, the try operator understands what you meant, early success is fine. Actually you can see this reflected in the larger language because break 'label value; exists in Rust unlike for example C++.
What makes Monad interesting is that it is a trait that you have implemented, so you can work generically any monads. I thereby feel like saying manually calling these methods is "monadic" kind of misses the point of why a monad was interesting in the first place.
Haskell's error handling isn't you sitting around calling monad methods: you implement the Monad trait so you don't have to, and it then all gets hidden behind do notation, with the result that you get the same control flow that you'd get just using exceptions.
It thereby is just constantly strange to me that people talk about any of these languages as if they learned something from Haskell... Haskell clearly wanted things like state and exceptions and such, but wanted to do so on top of a lazy pure functional core language.
The trick they came up with is thereby to define this trait called Monad, which lets you program into the control flow all of these bespoke behaviors you get from the imperative languages: state, exceptions, scopes, asynchronicity, list comprehensions... you name it.
But the end result is not in any way "manual": the end code doesn't involve destructuring an Either every time you make a call, but it also doesn't involve calling methods to deal with the errors. The end result is, as best as they could implement, exception handling!
And like, in the same way that people can make mistakes with manual memory allocation, so we prefer scope allocation, people can also make mistakes with manual error propagation, and so you'd expect we would prefer exception unwinding: the monad enforces consistency.
In that light, the behavior of most languages with respect to many of these behaviors is to just have hardcoded every function to be in a standard set of stacked monads for basic things everyone takes for granted: exceptions are just hardcoded monadic error handling.
But Rust? That isn't monadic error handling: that's just manual error propagation. If we are going to call Rust's manual error management regime "monadic", we should also call C's manual resource management regime "monadic". If you are doing it manually, it isn't monadic.
And sure, calling the methods of the monad kind of makes it look a bit less manual, but that's like moving on from C and now saying that Go's manual-ish resource management (defer) is monadic. If you aren't forced to do it the standard/correct way, it isn't monadic.
At least Haskell and OCaml also offer exceptions as alternatives.
I don't know if I'd say it's perfect. I'd still like a way to break on `Err` in debuggers for example.
.site-page.loading { opacity: unset; }It's far from perfect.
One of the biggest problems with Rust error handling is that if you want to have explicit error return types encoded in the type system you need to create ad hoc enums for each method that returns an error. If you only use a single error type for all functions you will inevitably have functions returning Results that contain error variants that the function will never actually return but still need to be handled.
Without enum variants being explicit types and the ability to easily create anonymous enums using union and intersection operators Rust enums require a ton of boilerplate.
I'm just now learning Rust, as a long time C++'er, and this was the first part of my Rust journey where I thought to myself, "Boy, this really smells--this couldn't possibly be the idiomatic Rust Way to handle functions that can produce different types of errors. I must be doing something wrong!"
For example, I have a function that takes an array of bytes, decodes it as UTF-8 to text, parses that text into an i32, and checks the int that it is within a valid range. This is not a big function. But it might produce one of: 1. str::Utf8Error, 2. num::ParseIntError, or 3. MyCustomInBoundsError. There's no clean way to write a Rust function that could return either of them. I had to bundle everything up into an enum and then return that, and then the caller has to do some "match" acrobatics to handle each error differently.
I hate to say this, but I miss Python and C++'s exceptions. How nice to just try: something and then:
An elegant weapon for a more civilized age.except SomeError: doFoo() except ThatErrror: doBar() except AnotherError: doBaz() finally: sayGoodbye()What do I know though? I'm still in the larval stage of Rust learning where I'm randomly adding &, *, .deref() and .clone() just to try to get the compiler to accept my code.
You can still do that in rust if you want / need to:
https://play.rust-lang.org/?version=stable&mode=debug&editio...
In library code though that would make it generally more difficult to use the library, so the enum approach is more idiomatic. Then that comes out asfn main() { match process(&[0x34, 0x32]) { Ok(n) => println!("{n} is the meaning of life"), Err(e) => { if e.is::<std::str::Utf8Error>() { eprintln!("Failed to decode: {e}"); } else if e.is::<std::num::ParseIntError>() { eprintln!("Failed to parse: {e}"); } else { eprintln!("{e}"); } } } } fn process(bytes: &[u8]) -> Result<i32, Box<dyn std::error::Error>> { let s = std::str::from_utf8(bytes)?; let n = s.parse()?; if n > 10 { return Err(format!("{n} is out of bounds").into()) } Ok(n) }
etc, which is isomorphic to the style you miss. What you're perhaps missing the is that `except ...` is the just a language keyword to match on types, but that Rust prefers to encode type information as values, so that keyword just isn't needed.match(e) { MyError::Decode(e) => { ... } MyError::ParseInt(e) => { ... } ... }I feel you on the larval stage. Once you get past that, Rust starts to make a lot of sense.
Many error types implement std::error::Error, maybe using that would make things easier.
An example: https://play.rust-lang.org/?version=stable&mode=debug&editio...
Hmm, I think I tried this a few times, but I could never get the right magical combination of dyn, Box<> and & to get it to compile.
This is true, though the issue of having error variants the function can't return is fairly overblown, because most code doesn't bother handling all error variants individually. Most of the time an error is either propagated upwards (possibly wrapped in another error) or logged. Inspecting the error variants is usually only done if you want to specially handle some of them (such as handling `ErrorKind::NotFound` when deleting a file), rather than exhaustively handling all variants.
We can disagree on it being overblown or not but I think it would be enormously useful to be able to look at a function signature and know exactly how it can fail.
They tried that in Java and no one uses it...
Lambdas broke checked exceptions. You can't declare that you throw whatever a generic lambda might throw, so they quickly devolved to "I throw nothing (only unchecked)." The "I throw everything" alternative is rarely used because it spreads virally through every caller.
Checked exceptions were strongly discouraged because they have nonlocal behavior when changing library code. If you want to rethrow exceptions you can’t handle you have to update all callers when the callee changes the throw signature. Lambdas are orthogonal.
I'm thinking of cases like Stream#flatMap where I might be prepared for what a lambda (probably a method ref) could throw, yet still can't use it because exception lists for the Stream interface methods had to be declared statically years ago.
Checked exceptions were vilified long before Java gained lambdas.
While Java usually gets the blame, it was following a precedent set by CLU, Modula-3 and C++.
Also I dearly miss them in .NET, every single time something breaks in production, because some developer didn't bother to catch exceptions, when they should have.
The ways in which a function can fail is typically the job of the documentation.
Why bother having a typed return value? That could be in the documentation too. The whole point of a type system is to help me understand what the function can and cannot do without needing to make guesses based on the documentation. It's not fatal, but it is annoying and inconsistent that Rust can do this on the happy path but not the error path.
The type system cannot capture 100% of the semantics of the function. You put what you can in there, but you also need documentation. You could provide a bespoke error type for every single function that returns an error, but that's a ton of boilerplate, and you're effectively just moving the documentation from the function to the error type (enum variant names are not sufficiently descriptive to avoid having to write documentation).
Even in cases where the error does already precisely match the semantics of the function, you still need documentation. std::sync::Mutex::lock() returns a `Result<MutexGuard<T>, PoisonError<MutexGuard<T>>>`. What's a PoisonError? That's a precise error type for the semantics of this function, but you need the documentation to tell you what it actually means for a mutex to be poisoned.
You cannot get away from having documentation. And you're free to make custom error types for all your functions if you want to, it just doesn't really get you much benefit over having a single unified error type for your module in most cases. If you have a reason to believe the caller actually does want to handle error variants differently then sure, make a new error type with just the variants that apply to this function, there's plenty of precedent for that (e.g. tokio::sync::mpsc::Sender has distinct error types for send() and try_send(), because that's a case where the caller actually may care about the precise reason), but most error values in Rust just end up getting propagated upwards and then eventually logged.
> You could provide a bespoke error type for every single function that returns an error, but that's a ton of boilerplate
If we had more typescript-like discriminated union semantics a lot of the boilerplate would go away. Throw in automatic implementation of From traits for enums composed of other enums / types and it could be pretty close to perfect.
Yeah. The error type requires so much boilerplate that I honestly thought I was stupid and doing something wrong. But nope. Just horrific amounts of boilerplate.
Then people who don’t want to engage with ThisError and Anyhow do bullshit hacks like making everything a string that has to be parsed (and don’t provide functions to parse).
I get why it is that way, but it feels icky.
> I get why it is that way, but it feels icky.
There is no reason it has to be so boilerplate heavy, a lot of it can be fixed but someone has to put in the work. The only technical reason that could hold it back is compile times.
Zig has automatic error unions. No boilerplate at all, but not just a single "error" type. The only downside I see in zig errors is that they can't hold extra data.
It's a massive downside. When I was using the JSON parser I found it very annoying that it could only tell me the input JSON was invalid, not where in the input the problem was.
Yeah I agree. I think they tend to go for a mutable parameter reference to keep track of that stuff, which is definitely C-like but kinda unwieldy.
Of course there is a reason it is this way. In lower level languages, compilers need to know the type and size of the type at compile time. This holds true even for languages with looser typing like C.
You are not going to get strict types in a low level language and also get ergonomic errors. This is fundamentally not how compilers works.