Should we "rust" exceptions?

58 views
Skip to first unread message

Miguel Oliveira e Silva

unread,
Apr 25, 2025, 7:03:58 AMApr 25
to eiffel...@googlegroups.com
As a follow up of the recent discussion on the (de)merits of the exceptions mechanism, I've dive a little bit into Rust and, specially, on its error handling approach [1].

Disclaimer: I should make it clear that I've never programmed in Rust (except trivial examples), and I'm still learning the language.  Thus, my presentation of the language features should taken with some grain of salt.  Nevertheless, I think I've learned enough of Rust to sustain my critiques [2].

First, Rust makes a categorization of errors which is, IMHO, a little bit deceptive.  It classifies errors as recoverable or unrecoverable.  It states that unrecoverable errors are symptoms of bugs in the code.  I can understand that in the presence of program bug, the approach should not be to recover from it, but simply to correct them (no disagreement here).  However, Rust does allow to recover from this alleged unrecoverable errors (catch_unwind), and, in fact, it is good in allowing that because, if one wants to develop a fault tolerant program, we absolutely need the ability to recover from buggy program parts.

I think that a better categorization would be to separate internal errors from external errors.  An internal error is one that is 100% the responsibility of the program; all others are external errors.  Null/Void de-referencing and (almost all) contract failures are internal errors; and no memory, IO errors, handling user input, program arguments, are examples of external errors.

Regardless of the programming language, and its support for specific mechanism for errors such as exceptions, since errors are (and always will be) an integral part of programming (hopefully, more concentrated in the development phase), we need systematic approaches to handle them.  I can identify (only) three alternatives [3]:

(1) The Ostrich approach;

(2) Defensive Programming (DP);

(3) Design by Contract (DbC).

Alternative (1) simply ignores the problem (much more frequent in practice than one may think). "If I don't go to the doctor, I cannot be sick" (good luck with that).

DP takes the view that a program should accept and be prepared for everything.  To achieve that, the program is flooded with non optional code to explicitly handle errors.  This code can rely on conditionals, error variables (like C's errno), exceptions, and whatever is available.  The view is to take error handling code as integral part of the code to be always executed.  Rust takes this view on error handling (although some DbC libraries can be found for Rust).

In this group I don't think I need to state what is DbC.  Nevertheless, let me declare that any code (whatsoever) has contracts [4]; the difference in DbC is that contracts are explicitly, and judiciously placed in the code (and that makes all the difference).  Contracts express the meaning of code (no sane code is intended to be meaningless).


DP has the problem of code contamination with error handling code (POSIX threads library, and many of the [mostly defensive] C, C++, Java, C# libraries, are clear examples); and, most of all, (too) many times falls into alternative (1).  (Let he/she/... who, among those of us who programmed in C, has never used the printf/scanf functions as if they were a procedure (void function) to cast the first stone!)

By far, the best approach for internal errors is DbC: simpler code; impossible, by negligence, to fall into alternative (1); possible -- without changing code -- to disable selected groups of assertions (making its execution much faster).  Together with a disciplined exception handling mechanism, we have no abstraction gap in interpreting exceptions (as happens with try-catch); ease to integrate in a fault tolerant code; just to mention some of its advantages related to error handling.  I've use DbC since the early 90's of the last millennium (immediately after, if I remember correctly, attending to an EUROPACE course given by Bertrand Meyer), and have never saw an example for which DP is preferable to DbC for internal errors.

External errors should be approached with DP.  Since, by definition, these errors are not 100% the responsibility of the program, then a reliable program must (always) protect itself from them.  Nevertheless, one can use contracts (assertions, and exceptions) to detect, and handle such errors.  However, some care must be taken: Those assertions should never be deactivated (or else, we fall into alternative (1)); and proper rescue clauses should be used (after all, there are some DP code with exceptions).


Getting back to Rust, are exceptions the good, the bad, or the ugly?

I think is quite clear that Rust takes the view that they are bad and ugly [5].  Thus, to avoid them, it prescribes the recipe that functions (may) return two different values (type Result<T,E>); one for normal code, and another for error(s).  Unlike many other defensive programming approaches -- one should acknowledge that -- Rust appears to make all the efforts to avoid falling into alternative (1).  However, it does not prevent code contamination with error handling code, neither (apparently) does it allow disabling "dead" error handling code (meaning, code that will never be applied). It also demands the price of extra complexity in function definition (Result), and, IMHO, a quite obscure syntax (!, ?).

The bigger issue of Rust is that, unlike Eiffel, error handling in Rust is a semantic-less mechanism (like try-catch exception mechanism), thus its deeper relation to code is mainly in the programmers mind.  In a language that fully supports DbC, contracts makes it all much more clear and simple.  Not only contracts express in the code the programmers design intentions, but also gives the unique possibility of the code itself to know if it is wrong (no ambiguity, and no external [human or machine] judge required).

Paradoxically, although Rust (strongly) criticizes exceptions, in practice, it seems to implement a quite similar control flow (instead of calling it exceptional execution, it simply panics [6]), also with the possibility of catching (rescuing seems a little bit more tricky, but my limited knowledge of the language might prevent me to see it clearly).

Finally, Rust error categorization is a little deceptive because it ignores DbC, and is not consistent.  No clear distinction between internal and external errors (we can panic in either of them, with no clear semantic differences in the code); we can have unrecoverable errors that might be external errors (thus, eventually requiring a recoverable DP approach); and (external) recoverable errors from which no recovering is desired ([1] is full of such examples).

Best regards,

-miguel

[1] https://doc.rust-lang.org/stable/book/ch09-00-error-handling.html

[2] But, of course, I might be proven wrong.

[3] In fact, only two systematic approaches exist, but since ignoring the problem is used widely in practice, and frequently Defensive Programming degenerates into this approach, I've keep it here.

[4] For example, no sane use of the real function sqrt is intentionally applied to a negative argument.

[5] I'm all for the good.

[6] panic! is the macro used in Rust to respond to (alleged) unrecoverable errors.
-- 
Miguel Oliveira e Silva
IEETA-DETI, University of Aveiro, Portugal

Ulrich Windl

unread,
Apr 25, 2025, 8:18:12 AMApr 25
to eiffel...@googlegroups.com
Well,

isn't the silent assumption that DbC does not need DP?
Sure, if proven correct, DP isn't needed, but who actually proves programs?
Running programs with full assertion checking on a few times doesn't prove anything. Also in reality such programs are run with assertions off.
So in case of an error they will cleanly abort and do no harm?
Bad things like hardware errors, targeted attacks, and unexpected user interaction (just to name a few) may happen any time.
How to deal with them?
Many years ago a professor had this solution:
If the function cannot provide the correct results, it will loop forever; so it will never return an incorrect value. True, but about everybody though instantaneously that this was a bad idea for real life.
Can something really bad happen, so that a later exception handler has no way to recover? Abort as cleanly as possible?

Miguel Oliveira e Silva

unread,
Apr 25, 2025, 10:11:10 AMApr 25
to eiffel...@googlegroups.com

Dear Ulrich,

Internal errors don't need DP;  they require DbC.

DP is needed for external errors (or else, the program will not be reliable).  In attachment follows three versions of a simple Eiffel program to print a list of prime numbers, following the three error handling approaches for a single external error: a program argument that is not an integer number.

It is a clear example that one should use DP for those categories of errors.  If an error is not 100% the responsibility of the program, then other than what is ensured by the type system (in the example: a string), one cannot assume nothing more (no postcondition in the argument(1) string value).  This is not the case, but sometimes it may be convenient to use of assertions to detect external errors.  In such cases, those assertions should never be disable and a proper rescue clause should be added (to avoid a cryptic error message to the program's user).

One can only prove the correctness (reliability, which also adds robustness, would be a better term) of a program with external errors iff DP is used.

Of course, if we can prove that a group of internal errors will never happen, then its associated assertions can safely be deactivated (no such possibility, by definition, in DP).  In general, no such possibility exists for external errors (unless we have external tools that ensure their nonexistence [1]).

Regarding the problem of dealing with possible (but very rare) external errors, one may, in the final product version, implement at the convenient level (execution topmost classes/routines) proper rescue clauses to "catch" such exceptions and provide more friendly error messages to the user (eventually with suggestions to solve the problem, or simple to request user feedback for future versions).

Perhaps it would make sense adding to the library default rescue handlers to provide such a common generic implementations (my apologies if it already exists).

-miguel

[1] Such as ensure that a program execution is guarded by a previous execution of a checker that ensures the desired postconditions attached to an external error.  But then again, the program does not know that, so DP is still required to ensure reliability.

show_primes.e
show_primes_dbc.e
show_primes_dp.e

rfo amalasoft.com

unread,
Apr 25, 2025, 10:31:42 AMApr 25
to eiffel...@googlegroups.com
This has been an interesting and, I hope, helpful discussion.  It has only reaffirmed my faith in DbC and discipline in general.
One thought that did arise from this is that maybe Eiffel should have one more assertion type (in addition to the current pre, post, check, invariant, loop, and supplier pre).
Having a kind of "check_always" assertion, independent of the existing ones, could give us what we might not otherwise get with the current arrangements and options.
One could say just use the current check assertion for those out-of-our-control exceptions and leave it enabled when you build your project, but projects include code other than the modules we just wrote.
Presumably even the "check_always" assertion could be turned off (can be important for code validation/analysis and testing) but would typically (and hopefully) remain enabled most of the time in the final compiled version.
Thanks
Roger


From: eiffel...@googlegroups.com <eiffel...@googlegroups.com> on behalf of Miguel Oliveira e Silva <m...@ua.pt>
Sent: Friday, April 25, 2025 10:11 AM
To: eiffel...@googlegroups.com <eiffel...@googlegroups.com>
Subject: Re: [eiffel-users] Should we "rust" exceptions?
 
--
You received this message because you are subscribed to the Google Groups "Eiffel Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to eiffel-users...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/eiffel-users/27718703-cea0-4120-a483-323e3b3b91ce%40ua.pt.

Ian Joyner

unread,
Apr 26, 2025, 7:24:02 AMApr 26
to eiffel...@googlegroups.com
Indeed this goes on. Jules May and I have continued the exchanges. He has written a book on it. He might indeed have some valid points. Here is our latest.

Jules May
Hmm. Of course I believe there are exception mechanisms. I just fdon;t beleive any of them deliver robist, stable software. They all amplify malfunctions, and create the brittleness that software is notorious for.
I, too, think there is value in separating normal code from abnormal situations. In fact, right through the book, I say “Protect the happy path”, and talk about multiple ways to achieve that (and ways to spot deviating from it).
You can imagine such a thing as 100% correctness, even if you can never actually achieve it. But 100% stability? No, you're making a category error here. Stability (in various forms) is how you can deliver correct behaviour, even in a malfunctioning system. Of course, everything has its limits. But in a stable system, you don't need to imagine every way in which the system can malfunction: you can let it malfunction and keep going on-plan anyway. That’s a very powerful idea, and a very powerful way to build safety-critical software. It's also a great way to build software cheaply and quickly.
IME most programmers are just pounding out code and trying to look better than their peers so as not to get fired. Bad software, unfortunately, delivers a modicum of job security.
——
My response:
“They all amplify malfunctions, and create the brittleness that software is notorious for.”
You might be right, but I’m still skeptical. On the surface that says software is brittle and software has exception mechanisms, therefore exceptions are the cause. Or that because exceptions are the way to handle malfunctions that they are amplifying them.
I think other factors to be considered are that programmers are using defective exception mechanisms, they are using them for the wrong things, and simply poor programmers.
But I’m not saying you are wrong, but there is a lot to be considered.
You have also stimulated some discussion on the Eiffel group. Particularly, some discussion on the fact that Eiffel contract checking can be disabled for production software. This is for performance factors. Run-time checking has an overhead.
C.A.R Hoare noted that customers of Elliott ALGOL wanted checks like bounds checks to be left on for production software.
Since Eiffel’s exceptions may be disabled, I think you have a point that really they are not for robustness (stability), but for correctness.
I agree that a run-time mechanism is needed to that programs can recover from lower errors. Perhaps my thinking is coloured by Burroughs system software and how these provide robust, reliable, and secure systems, where interrupts can be enabled to recover from situations (but as I pointed out before, resource depletion — memory and disk — resulted in a call for operator action, not just unceremoniously dumping a program, or triggering and exception for the program to happen, as in Unix, which I consider the wrong thing to do).
I can’t remember anyone enabling interrupts to handle an out-of-bounds case, which would crash a program. But maybe that would be done for production software. However, the environmental software (middleware) would most likely have been charged to restart processes.
In most cases it is a mistake to push that into application code. However, for embedded code or running a plane that might be different.
Now, I think there is more I agree with in your paragraph about keeping systems going in the case of malfunction. If the way to build software cheaply and quickly is to not burden application programmers with needing to handle everything, then I agree. But that means that all applications must have such restart and recovery code centralised in some kind of middleware.
For really critical applications, like running a plane, I would hope there are several computer systems, in case one fails at the hardware level. Redundancy is also a key to robustness.
“IME most programmers are just pounding out code and trying to look better than their peers so as not to get fired. Bad software, unfortunately, delivers a modicum of job security.”
I agree there. Programmers are overly concerned with matters of image and ego. Managers want things done quickly rather than quality. There is general lack of discipline in programming and people who think it is magic incantations and things will work.
Ultimately, I think we are both concerned about the dire state of current software. The factors involved must be correctly identified.

Ulrich Windl

unread,
Apr 26, 2025, 10:59:12 AMApr 26
to eiffel...@googlegroups.com
Hi!

I mostly talk about my own personal experience in multiple languages.
I strongly believe in not ignoring any error, so I realize that most of the "if then else cascades" are not for solving the main task, but to handle all kinds of errors that may occur.
Then exceptions seem to be quite handy: Wrap any block of code with exception handling, allowing to tell "something went wrong" and return with an error (recursively).
That works quite well with one's own code, but with today's powerful toolkits there's a tendency to wrap code with exceptions at a very high level, so if something fails, it's really hard to tell what went wrong actually.
Bad example from some popular software (I won't name here):
Programs call a "remote" function by making a network connection using some proprietary protocol. If for example the remote program does not exist, there's no error message saying so. Instead nothing happens for quite some time, until a "timeout error" is reported eventually. Still such error could mean the remote program started successfully, but took very long to send back the results, or the program wasn't started at all.
A very interesting related question is how to report failures back to the user.
Imagine Windows is just reporting "the application crashed": Will the user ever have an idea what went wrong?
OTOH if (for example) a user is scaling some large image, and the error reads "cannot create temporary file", he may wonder why a temporary file is needed at all, and still would be rather clueless. If however the error message would include the file name and the error code  (reason), it might help the user.
"File already exists" may indicate a program error
"Permission denied" may indicate a configuration error, and the user might be able to fix it
"No space left on device" may be an error the user can fix

Error handling is a rather complex problem, and understanding what a reported error actually means is a different problem.
For example the infamous output of alternating zeros and ones that were meant to indicate an error in the Ariane rocket, but then the pattern had been interpreted as course correction data (in my plain words)

Exceptions CAN be useful, but they cannot solve all kinds of problems.
Some years ago I had written an interpreter for some special computing language in Eiffel, and I was very proud to have implemented an exception mechanism in that language that allowed to catch exceptions of the implementing interpreter in the interpreted language. So the program wouldn't just abort on an interpreter error, but could trigger a special reporting routine for the user.
It could even "retry".

Kind regards,
Ulrich

Miguel Oliveira e Silva

unread,
Apr 26, 2025, 7:28:38 PMApr 26
to eiffel...@googlegroups.com

Who defines the "happy path" of the code?

Is it a program execution outside error handling?  A non-exceptional execution in languages supporting exceptions?  A case-by-case scenario?  (In DbC, if I understand correctly "happiness", is simply a program running without assertion failures.)

When in "unhappy" state, how does the code separates a(n implicit) precondition failure (client's responsibility), from other failures (supplier)?  This distinction is absolutely critical for tracking and handling errors.  I suspect that, without DbC, such decision will be judged outside the program by its programmers.

In DP there is no clear distinction between normal "correct" code, and code used for internal error handling.  Also, different classes/routines will most likely use different error codes/mechanisms for each source of error, so probably there won't be a systematic way of dealing with them, making error handling even more complex (and ad hoc).

No such problems and ambiguities exist with DbC.  In DbC the answer is clear and very simple (to the point, as I have already mentioned, that the program itself knows if it is being executed "correctly" [1], without requiring an external -- human or automatic -- judge).  Of course there is no magic involved here, such result is achieved because contracts are redundant to the code [2].  To my best knowledge, in engineering redundancy serves only two positive purposes: fault detection, and fault tolerance [3].  Redundant contracts allow achieving both purposes (assertions to detect failures; and redundant algorithms/classes within proper rescue clauses to approach -- with extraordinary simplicity, and no ambiguity -- fault tolerance).

--

(Am I missing something, or "stable software" is simply being used to mean reliable software (correct & robust)?)

--

One last argument.  I think that exceptions, or any other error handling mechanism, only amplifies the malfunction of a program if they make more difficult, or even prevents, error handling.  That is the case if it obscures, or even hides, the source of errors.  Such a problematic thing does not happen with DbC and a disciplined exception mechanism [4].  As I've showed before, such problem may exist with try-catch alike exceptions, but also with DP (because there is no clear difference between precondition error from the remaining errors).  In fact, we may define DP as a no-precondition approach to programming (accepts everything).  IMHO, that's nonsense (except, of course, for external errors).

-miguel

[1] Always considering the existing expressed contracts (hence the "").

[2] The exception being concurrent contracts. Those need to be implemented as conditional synchronization points, thus, they cannot be ignored.

[3] All other uses of redundancy are, most likely, bad (and ugly).  In fact, I strongly suspect, that we probably can correlate many of programming language history and evolution with a reduced redundancy in their new mechanisms and methodologies.

[4] In a fault-tolerant program we may even choose the level at which we want redundancy -- close to each possible failure (in general, not a good idea) -- or at an upper more abstract level; in either case to goal is always to ensure the respective postcondition&invariant.

Reply all
Reply to author
Forward
0 new messages