Today I thought I would experiment with catch_unwind. I understand that in Rust, explicit handling of exceptions/errors using Result has long been the preferred solution, but catch_unwind seems to work just fine for me as well.
Right now I have a slightly uneasy feeling, and am deciding whether to proceed with it or to stick with Result. What's your experience been? Should I turn back before it's too late?! It certainly saves a certain amount of typing. What are the disadvantages?
Result wherever you can. This is idiomatic, and self-documenting. Using panics for "normal" error handling will be surprising to every Rustacean reading your code.
Panicking/unwinding in Rust is for bugs. It should happen only if the programmer wrote something incorrectly.
catch_unwind exists for last-resort cases where you can't ensure "proper" handling of errors (e.g. it's necessary in web servers to isolate failing tasks and prevent them from taking down the whole server).
If you’re writing a binary and you just want simple error handling because the specifics of which the returned error isn’t that important but you want to show the error message and maybe perform some cleanup, just use the
anyhow crate. It makes error handling pretty simple. You lose some of the benefits of creating a custom Error type such as letting you handle different kinds of errors in particular ways, but that often isn’t important in a binary. And you can always incrementally add back in custom error types for parts of the code if it turns out to be necessary.
Panicking will always print a message, this is a bad thing for expected errors like the remote end of a tcp connection closing the connection. If using
RUST_BACKTRACE=1 panic messages even have a backtrace. (I have
export RUST_BACKTRACE=1 in my zsh profile to force it for all rust programs, useful if something panicked when I wasn't trying to debug it yet.) Unwinding is slow and on Windows I believe backtrace generation can take seconds in some cases.
catch_unwind doesn't work when you compile with
-Cpanic=abort reduces the binary size and improves runtime performance by a couple of percent.
The only valid use cases for
catch_unwind IMHO are for FFI boundaries to propagate the panic in a way that is not UB like unwinding over
extern "C" functions is (will be aborting in the future) and when you have got something like a server where you can simply return a 500 http error code and should likely keep serving requests even after a bug caused the current request handler to panic.
It's a parser/interpreter, running as a web server. The errors have the name of the routine or batch being parsed, type-checked or executed, with an associated line number.
Either method seems to work perfectly well, but there is less "boilerplate" in the exception version of the code - I don't have to declare a result for each function (many functions don't have a result at all), and I don't have to type a load of "Ok"s and "Err"s and question marks. I think it's easier to read the code, as there is less "noise", and it's slightly easier to write it ( I often forget "?" until the compiler reminds me! ).
My preference currently is for the exception based code, unless there is a solid technical reason to use Results - I haven't found one yet. But this seems to go against the Rust idiom. So I am hesitating.
That is my scenario, and given I need to handle exceptions anyway, it seems simple to handle them uniformly, and save quite a lot of typing. I haven't noticed unwinding being slow. Exceptions should be very, very rare ( apart from trivial syntax/type errors in the interpreted language, and even those should be just a handful per week ).
In your case the errors from the parser/interpreter are expected and not the result of bugs in your server. You probably need to handle both kinds of errors separately anyway. You probably don't want to give a 500 internal server error when the user input is invalid. It should probably be more something like 400 bad request with a nice error message.
Assuming that you are using rocket, panics are already handled for you and turned into 500 internal server error.
Common cases are:
For logical errors, use
Result. Example: user supplied invalid date as an input ->
For environmental errors, use
Result. Example: database process you are communicating with gets killed, and the corresponding
read from connection to the database returns an error ->
For programmer's bugs, use
unreachable! when an internal program invariant is violated.
Less common cases:
If you are writing something critical, where it is super-important that fatal environmental errors shut the thing down and and are not silently converted to data corruption, it might be better to panic on fatal environmental errors. Logic errors are recoverable (and are often recovered), using the same
Result for them creates a risk of mishandling the error. Although, if you are in control of such correctness-critical code, the best solution is to use
Result<Result<T, LogicalError>, FatalEnvironmentError> (see Error Handling in a Correctness-Critical Rust Project | sled-rs.github.io).
If you are writing something critical and embedded into another application where it's paramount that you don't bring the host down, it might be better to use
Result for programming errors. Ie, you can ban
 operator (which pancs), and just always use
get. The better strategy is to panic in debug and return a neutral answer in release: The Use Of assert() In SQLite
If you are writing something where availability is important, you need to make sure that bugs in software don't bring the software down. If that's the case, you need your application architecture to support strict state management, such that most of the code can't mess-up state. Than you can wrap that "most of the code" into a
catch_unwind. Example: a web-servise where each request is running inside catch_unwind inside transaction. Recovering from bugs is fine as transaction gets aborted.
rust-analyzer had a great success with using
catch_unwind to implement cancellation. Cancellation is not an error, it's a serendipitous success, but unwinding is a good match for it.
Right, but they should never happen. All the exceptions are handled ( unless something very awful happened ) and are specific to the interpreted code that is being compiled or executed. They are not really "user" errors either, they are programmer errors ( by the software engineer who is maintaining the system, usually me, and in any case, most of the code is machine written, so they are not really expected ).
I'd be curious about what you'd think of using
#[fehler] to replace the exceptions with it. You will still have to use
? (with a
#![warn(unused_must_use)] you should always be able spot the "statements" where you might forget them), but the signature of the function will be a bit more lightweight, you won't need to
Ok() wrap, and you will be able to use
throw!(err) rather than
return Err(err);, basically "tainting" the
Err variant as more of an exception.
I would have a lot of "#[throws(MyError)]" lines throughout the code. Nothing major, but still a bit of a distraction. I think on grounds of simplicity/non-obscurity I'd prefer not to use macros unless there is a significant benefit. As far as I can tell, catch-unwind fits my needs perfectly well.
The fehler "About" claims "Rust doesn't have exceptions"! I think that's a trifle misleading. It has two official exception handling systems, Result/Err/? and the more recent catch_unwind.
I can see that for some situations catch_unwind might be quite unsuitable. I don't think I am in one of them. Or if I am, I haven't realised it yet!
One con that hasn't been mentioned yet is that “double panic” (panicking during unwinding) will always abort the entire process, and cannot be caught. So if you are using panicking a lot for error handling, and relying on unwinding, then you have to be very careful about what code runs in destructors.
Of course, internal server errors should never happen, but they occasionally do anyway. There's a difference between "user pasted his thesis into the input field instead of an expression" and "programmer forgot to check the size and indexed into an empty slice". The first kind of error is not exceptional, it is expected that users will occasionally do dumb stuff accidentally or on purpose, and the user should expect some feedback about what they did wrong. The second kind of error is exceptional, its existence should have the least possible impact on happy path performance, and whatever internal server details are relevant should be logged and the user shown a 500 page.
Of course you can distinguish between kinds of panics. That means you must be doing some checking at
catch_unwind, which means you're either using
panic_any with custom types and downcasting it (ew) or parsing the string representation (double ew). I guess I can see how not having to use
-> Result<T> might add up over a deep call stack but... in terms of error handling, I guess I think of most of the boilerplate being in creating custom error types, messages and conversions, which just surfaces in another form if you are doing matching on panic types at the top level.
I think "no panics in drop" is a good strategy no matter how errors are handled. Because it's such a minefield otherwise (unless you really don't mind immediate aborts).
I agree, and this strategy is fairly easy to follow in idiomatic Rust code, where panics are rare and can only result from incorrect code. But it would become harder to follow if you are using panics pervasively for all types of error handling, so almost any function might panic even in correct code.
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.