There's a critical difference between exceptions and what's happening in this article: exceptions create de facto nondeterministic behavior in programs. They cause every line in a function to potentially result in a return from the function with an unexpected type. Rust's error handling requires explicit return statements and explicit return types. This critical difference results in code that is far easier to document, reason about, and slightly better performance as well.
If calculation2 was previously initialized to a default value, then how would we know if the calculation was completed before the exception was thrown without adding another 8 lines of boilerplate? This is compounded by other functions being able to throw their own exceptions. Consider:
If both the display() and log() functions might throw IO exceptions, then how would we know whether or not the error was logged, even if the exceptions were checked, unless we create custom exception types for every possible error?
In conclusion, we don't know with certainty which path was taken through the code's execution, and this is tantamount to non-deterministic behavior.
Non-determinism is when for same input you get a different output. According to that definition, the above code is deterministic.
> how would we know if the calculation was completed before the exception was thrown without adding another 8 lines of boilerplate?
You can't, exceptions or not. With exceptions, you need try..catch blocks. With return values, you need ifs.
But with exceptions, you have an option of not handling an error where it originated, without completely ignoring the error.
With error results, you can handle the error (which usually means just returning it to the caller) or ignore it (potential bugs). In languages with not-so-strong typing, it can be very easy to ignore the error. In languages with strong enough typing, you must handle the error. This can be made nicer with syntax sugar, but still leads to boilerplate. This also has an unfortunate consequence of more branching in the generated assembly, which can lead to poorer performance on the "happy path". Exceptions also have cost, of course, but it's geared more towards making the happy path fast and exceptional path slow, which seems like a better tradeoff in most cases.
> If both the display() and log() functions might throw IO exceptions, then how would we know whether or not the error was logged
But without exceptions, your code is simply ignoring any potential errors in display(). And in log() for that matter. If they threw exceptions, it would be impossible to just "swallow" an error silently.
> In conclusion, we don't know with certainty which path was taken through the code's execution, and this is tantamount to non-deterministic behavior.
I would disagree with that. People sometimes see exceptions as somehow "breaking" the program or leading it to an inconsistent state, but that's simply not true.
All you need to realize is that...
1. Every line can throw an exception unless proven otherwise.
2. You don't need to handle an exception close to where it was thrown in most cases. Corollary: you shouldn't use exceptions for control flow. If you notice that your callers use try..catch directly around your function, that might be a sign you should use return values, not exceptions. Return values are not forbidden in languages with exceptions!
3. But you do have to dispose of unmanaged resources. This is probably the main sticking point and does require some discipline.
...and suddenly exceptions stop being awkward and become quite nice to work with.
Rust still has panics which is more or less the same mess. It is not as bad though but still annoying especially when writing libraries. When writing a binary, it is possible to just set panic to abort at least