> Great example of Rust being built such that you have to deal with error returns and think about C++-style exception safety.
Not really. Panics are supposed to be used in super exceptional situations, where the only course of action is to abort the whole unit of work you're doing and throw away all the resources. However you do have to be careful in critical code because things like integer overflow can also raise a panic.
> be careful in critical code because things like integer overflow can also raise a panic
So you can basically panic anywhere. I understand people have looked at no-panic markers (like C++ noexcept) but the proposals haven't gone anywhere. Consequently, you need to maintain the basic exception safety guarantee [1] at all times. In safe Rust, the compiler enforces this level of safety in most cases on its own, but there are situations in which you can temporarily violate program invariants and panic before being able to restore them. (A classic example is debiting from one bank account before crediting to another. If you panic in the middle, the money is lost.)
If you want that bank code to be robust against panics, you need to use something like https://docs.rs/scopeguard/latest/scopeguard/
In unsafe Rust, you basically have the same burden of exception safety that C++ creates, except your job as an unsafe Rust programmer is harder than a C++ programmer's because Rust doesn't have a noexcept. Without noexcept, it's hard to reason about which calls can panic and which can't, so it's hard to make bulletproof cleanup paths.
Most Rust programmers don't think much about panics, so I assume most Rust programs are full of latent bugs of this sort. That's why I usually recommend panic=abort.
[1] https://en.wikipedia.org/wiki/Exception_safety#Classificatio...
Rust's number types have functions like "wrapping_add" or "overflowing_add", which do not panic when overflowing and instead explicitly wrap around or return a result that must be checked.
You can easily write code that does not contain any possible panic points, if you want.
I don't think it's quite as easy to guarantee panic freedom as you think.
For example: do logging frameworks guarantee no-panic behavior? People can add logging statements practically anywhere, especially in a large team that maintains a codebase over significant time. One innocuous-looking debug log added to a section of code that's temporarily violated invariants can end up putting the whole program into a state, post-panic, in which those invariants no longer hold.
A lot of experience tells us that this happens in practice in C++, Java, Python, and other excpeption-ful languages. Maybe it happens less in Rust, but I'd be shocked if this class of bugs were absent.
Note that I'm talking about both safe and unsafe code. A safe section of code that panics unexpectedly might preserve memory safety invariants but hork the higher-level logical invariants of your application. You can end up with security vulnerabilities this way too.
Imagine an attacker who can force a panic in a network service, aborting his request but not killing the server, such that the panic on his next request grants him some kind of access he shouldn't have had due to the panic leaving the program in a bad state.
I'm not seeing Rust people take this problem as seriously as I think is warranted.
> A safe section of code that panics unexpectedly might preserve memory safety invariants but hork the higher-level logical invariants of your application
The usual way of dealing with this is to use impl Drop to cleanup properly. Resources are guaranteed to be dropped as expected on panic unwinds. Eg the database transaction rolls back if dropped without committing.
> Imagine an attacker who can force a panic in a network service, aborting his request but not killing the server, such that the panic on his next request grants him some kind of access he shouldn't have had due to the panic leaving the program in a bad state.
You need to be more specific. Why would the web server be left in a bad state because of such panics (in safe rust). All the memory will be cleaned up, all the database transactions will be cleaned up, mutexes might get poisoned, but that's considered a bug and it'll just cause another panic the next time someone tries to lock the mutex.
Sure, it's not trivial, but plenty of people who need this seem to do it.
https://crates.io/crates/no-panic
Huh. That's neat.
> If you panic in the middle, the money is lost.
Wouldn't a relational database help deal with this?
> Without noexcept, it's hard to reason about which calls can panic and which can't, so it's hard to make bulletproof cleanup paths.
Unsafe blocks usually contain very low level code, so you can understand what your code does very accurately. If the unsafe code calls a dependency which calls 150 other dependencies transitively, yeah, that's going to be pretty bad.
> Wouldn't a relational database help deal with this?
Sure. It's just a toy example. There are lots of real programs in which you temporarily violate invariants in the course of performing some task, then restore them after. Another example that comes to mind is search tree rotations.
> Unsafe blocks usually contain very low level code, so you can understand what your code does very accurately.
Perhaps at first, but code evolves.
> However you do have to be careful in critical code because things like integer overflow can also raise a panic.
This is incorrect. Only in debug builds does it raise a panic. In release Rust has to make the performance tradeoff that C++ does and defines signed integer math to wrap 2’s complement. Only in debug will signed overflow panic. Unsigned math never panics - it’s always going to overflow 2’s complement.
> Only in debug builds does it raise a panic.
Correctness in debug builds is important, isn't it?
That said, panic on integer overflow in debug builds is unfortunate behavior. Overflow should cause an abort, not a panic.
> make the performance tradeoff that C++ does and defines signed integer math to wrap 2’s complement
In C++, signed overflow is undefined behavior, not wraparound. This property is useful to the optimizer for things like inferring loop bounds. The optimizer has less flexibility in equivalent Rust code.
You can choose whether panics immediately abort, and you can also choose whether integer overflow panics in releas builds.
Personally I would often choose both, overflow panics and also panics abort, so if we overflow we blow up immediately.
What's the rationale behind aborting and not panicking in debug? Unwinding and flushing buffers seems like a better default with debug binaries.
You can enable overflow panics in release build, so if you're a library, you have to play it safe because you don't know how people will build your library.