I don’t know rust, could this have happened with Ada?
I did not read much further than the headline that it was .unwrap(). AFAIK, yes, it could have easily happen to Ada. Basically, .unwrap() says “I know this could throw an exception, but I am not going to care/handle it”. So the exception that was generated went up the program/system and crashed it. Basically, Ariane 5 version 2.0.
Could have Ada prevented it? No, bad code is bad code. Could have SPARK prevented this? Yes, with the Exception analysis system that was integrated a couple of years ago, one could have caught these issues.
Best regards,
Fer
SPARK adds a safety layer, but you still can have an error in the formal specification, which actually was the case of Ariane, when the specifications became erroneous after hardware upgrade. So SPARK would not help.
Ariane blew up because the management decided to save money (look at how well that worked out) by utilising a package from the previous ariane which was incompatible.
An unwrap doesn’t cause an exception since Rust doesn’t have exceptions. It causes a panic. Panics terminate a process, and generally cannot be handled. That’s why I wouldn’t use Rust in a server. In a language that raises/throws exceptions, there could be (handle-/catch-all or handle-/catch-most) exception handlers at boundaries that allow for restarting tasks. For example, In a prototype I wrote in Python, an RPC library threw undocumented exceptions, which would have caused the whole server to terminate; however, I started the RPC service in a loop that restarted the service whenever almost any exception was raised and unhandled further down the call chain. In that way, processing of user requests that had already begun in other threads continued uninterrupted and only the requests currently being received by the RPC service would be lost. In any application (in a language with exceptions), there could be boundaries at which handlers could be placed to handle catastrophic errors (or just unexpected exceptions) in subcomponents without terminating the whole application. But that might not be possible in general with Rust panics. Therefore, it doesn’t take extraordinary discipline of handling all error conditions at the most specific/appropriate points in the code.
I have heard that the Linux kernel developers did not allow Rust into the kernel until a special version without panics was written because they were unwilling to allow the Rust panics into the kernel.
In a language that raises/throws exceptions, there could be (handle-/catch-all or handle-/catch-most) exception handlers at boundaries that allow for restarting tasks.
This is precisely what libraries like tokio do in Rust. Panics are uncatchable in very specific conditions:
- a panic in a destructor during unwinding cannot be caught (double panic, essentially)
- no panics can be caught when compiling with
panic=abort(and you wouldn’t compile a server with that) - a panic across the FFI barrier cannot be handled properly
But panics are not a control flow feature, and when a function panics it usually means that the program is in an unrecoverable state. The use of unwrap() by cloudflare was bad error handling that no language can save you from.
That’s very interesting. Thanks for clarifying that. The documentation that I’ve read said that not all panics can be caught (with, e.g., catch_unwind) and did not elaborate on which panics could be caught and which could not. It seemed to imply that there were no hard rules. Are the rules you stated universally true in Rust?
A language that allows static checks for unhandled error conditions could have saved them. I’ve seen a post or a few in the Rust forums asking how to achieve that.
The rule (as I understand it) is that regular unwinding can always be caught by catch_unwind. But unwinding across FFI is UB, and panicking during unwinding cannot start a new unwinding panic. Panics are implemented like exceptions, so their behaviour is not much weirder than exceptions in other languages.
I work on a team that uses Rust to build a server, and it never panics from an .unwrap because our coding standard & peer review forbid the use of .unwrap, so that we therefore handle or propagate every single Result and Option type. Rust’s type system forces us to handle those objects: they’re right there in the function signature. We’ve gotten pretty good at that; I can’t remember the last time I had to PR a commit where a lazy dev tried to slip an .unwrap into the code.
What’s more, the standard Rust toolchain pitches a fit if you don’t document the possibility of an .unwrap or a .panic.
By contrast, Ada doesn’t force you to handle exceptions, and its function signatures don’t even indicate when a function might raise an exception, so I suppose it would be fair to say that I would much rather write a server in Rust than in Ada? ![]()
(I don’t know if it’s fair to say that the standard Ada toolchain, i.e., GNAT, won’t pitch a fit if a function either raises an undocumented exception or fails to handle an exception raise by a function it calls, but I don’t remember it ever complaining about it. Then again, neither would I seriously say that I would rather write a server in Rust than in Ada.)
The Rust-based server I work on can panic on account of arithmetic under/overflow, index errors, etc., which is why we eliminate most indexing via the use of iterators. (If we have to index, we use .enumerate, so whose indices are guaranteed correct.)
Again, Ada will likewise terminate in such circumstances, and it has no .enumerate capability to my knowledge – there’s 'First and 'Last for arrays and .First_Index and .Last_Index for Ada.Containers.Vectors, which is both inconsistent and a little harder to work with.
Added later: I concede that .enumerate will not solve all issues with indexing; I can readily remember a project I worked on where that would have been impossible. In our use case, however, it’s borne the brunt of most of our problems, and .clamp has usually done the rest.
In the “Notes” section of this documentation is where I got my information. It puts “might not catch all panics” in bold and explains that some panics are implemented as aborts rather than using unwinding and does not say that there is a way to know which implementation method was used for a given panic. Elsewhere, in the Rust “book”, it doesn’t even mention that some panics can be handled, so the documentation seems quite limited in that regard. Furthermore, with no spec, how can one be sure the implementation won’t change?
That’s why, prior to your comment, I had assumed that it is not safe to rely on Rust code not to terminate unexpectedly. IMO, in general the author of a subprogram cannot know whether something is recoverable unless they also wrote the code that calls that subprogram, and even if it can’t be recovered, one might want to print/show a user-friendly message or do logging.
Unfortunately I don’t know Rust very well even though I read the Rust book a long time ago. I’m also new to Ada. Is .enumerate a way to do a foreach loop? Is for I in My_Array'Range loop not an Ada equivalent?
Isn’t that an AI in waiting?
Not sure how to answer, but it works like this:
for (ith, element) in impls_into_iter {
assert!(imlps_into_iter[ith] == element);
}
No.
- For an array, it’s close, but you still have to declare a variable and reference the element.
- For a vector, or any other container, there is no such construct.
That doesn’t help me today.
I think there is something like that in Ada 2012:
for E: Typename of Iterable loop
It is a complete non-issue: you will put a catch-all handler with "exception when others … " at the right place and that’s it.
Time to add your thoughts to get it added to the next revision, if you’re still alive by then ![]()
This discussion is decades old. It always starts from a wrong point and then proceeds into nonsense.
The talk must be about composition of the contracts, substitutability (inheritance), conditional contracts, for example when Integrate receives F as an argument its contract is I raise this, that and what F raises. Trusted contracts, e.g. when the programmer promises certain exceptions to occur or not occur without proof, I/O exceptions for example. Obligations, e.g. exact conditions when a certain exception must propagate.
How does this obtain E’s index?
It doesn’t. Sorry. I missed that. It only gives the element.