Zero-Cost Exceptions and Error Interfaces
sourcery.ai
sourcery.ai
For example, Rust has a Result type [1], so this function would return Result<Item, ItemNotFound>. This forces the caller to think about what to do in case of an error and specify what should happen.
Also, Rust culture (IMHO) tends to be more strict about technical errors, probably because it is applied to more low-level problem areas. If high-level business logic is cluttered with technical error handling in every other line, it becomes near impossible to read and maintain, so other patterns are applied (e.g. atomic transactions that are undone when something fails).
Usage and culture and affordances, as in:
- the stdlib doesn’t signal errors by panic-ing
- rust learning documents drive users towards Result and Option
- the standard library provides APIs to manipulate results in ways Java is utterly missing e.g. map_err, ok, ok_or, ?, …
- because results are reified values, the language allows abstracting over results (creating macros or functions around them), as well as providing utilities around them (e.g. eyre, thiserror, anyhow, …)
- because results are reified values, they can be ignored until relevant, an `Iterator<Item = Result<…>>` is just an `Iterator<Item = _>` until you actually need to process the result.
- panics have very little signaling power, technically your panic can carry a payload but I’ve never seen anyone go beyond a message
Also rust panics are explicitly noted to not necessarily be recoverable from, so they can not be used for error signaling: whoever compiles the program can set panic=abort, and now you have no error handling at all, the program just straight dies.
And even if you actually, genuinely, want to use checked exceptions in Java, it’s hell, because it does not mesh with the rest of the language. Just look at the contorsions necessary to get checked exceptions through Stream APIs.
Checked exceptions are not necessarily bad in and of themselves, but Java’s implementation is so awful it has completely poisoned the field. And frankly it just… doesn’t seem like a very useful thing to put work into: you can just use exceptions-flavoured results à la Swift or Zig if you like the “look” of exceptions.
This was worked around in the design of CompleteableFutures, where the closure must cyclically capture its own future in order to be able to call .completeExceptionally on it; but that requires that every use of functional code essentially subvert Java by having a try-catch at the root of the closure’s AST that turns propagating exceptions, into, essentially, results, via a side-channel; and then the caller of CompleteableFuture.get gets the side-channel exception value thrown back at them and propagating again. Honestly a mess—and people think Golang’s little “if err != nil” checks are bad!
(What should actually be happening is “exception generic auto-deduction”: anything that accepts Callable or other functional interfaces should actually accept a (currently-impossible) type “Callable throws <T>…”; and then the list of checked error types of the instantiated object should be deduced from what the called code actually does—as Java perfectly-well knows how to do, in order to assert compiler errors about checked exceptions in the first place. This would also require you to be able to define methods that “pass along” checked exceptions without knowing what they are, though — and without just declaring you throw Throwable and thus discarding the exception typing of the callee — in order for the framework code that takes these Callables to be valid.)
"It is not recommended to use this function for a general try/catch mechanism. The Result type is more appropriate to use for functions that can fail on a regular basis. Additionally, this function is not guaranteed to catch all panics, see the “Notes” section below."
> atomic transactions that are undone when something fails
This is exactly what many business apps do frequently, through the use of database transactions.
[1] https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
And while C++ has moved away from its original design, it kept the variant of "no exceptions" or "expect any kind of exceptions", and still there are ongoing discussions of eventually getting something similar to Swift's model.
Also, the type system isn’t strong enough in most cases to express something being generic over the exception type, so lots of interface implementations end up having to wrap every exception in a RuntimeException anyway.
There is definitely a trade off for efficiency and guarantees about all error conditions being handled. That means that exceptions aren't appropriate for all situations or perhaps even for all languages. But taking such a hard line against them is just as naive as using them too much. (It's always suspicious when you see someone making a sweeping declaration like this about engineering trade offs.)
Python is not Rust. Its goal is not to be hardened and super reliable. Its goal, at least sometimes, is to maximise developer productivity. Imagine telling a Python developer who's just thrown together a quick script that, actually, no, you're going to have to waste time making it more verbose so that it satisfies some reliability constraint you don't need. It just doesn't make sense.
The implementation proposed in the article seems very reasonable python.
Writing code for language X in the style of language Y is usually an antipattern. When Rome do as the Romans do.
In C, if it can fail, my function always has an error out parameter.
void mtt_read_user_file(char* path, mtt_error_t* error);
The ease of use benefit of having a unified way of checking for errors far outweighs any performance cost.My errors:
- are binary. "FAIL" vs "SUCCESS"
- hold a chain of user friendly error strings
- have caller controlled behavior, such as turning off error strings to save perf
- can also hold a single error code
- supports being fully preallocated, which I always do at thread creation time. I only need one error object per thread.
I'm quite proud of the error handling in my C code.
If that was the goal, shouldn't we like checked exceptions the best?
The point of the article is that, at least in python, skipping the result check is cheaper in the happy path and slower in the exceptional case which is a useful trick. You haven't addressed that trade-off at all.
At some point you realize that exceptions, error codes and the error monad are all equivalent except for syntax and with enough syntactic sugar it should be possible to unify them.
load r0, some_location
may encounter the error that some_location is in a not-present page that needs to be faulted in.Should the instruction expose that through its interface, like some special value stored in r0, or a CPU flag being set that needs to be tested?
I'd suggest to change your example to faulting for a page that doesn't exist in swap either, so a decision has to be made whether to set a flag or e.g. send a signal to the process.
The transparency is a great benefit, which can be attributed to exception handling.
> I'd suggest to change your example
That is hardly required for my point.
> send a signal to the process
That's another exception mechanism.
Like Java's "throws" keyword?
For general results cases (i.e. non exceptional cases), there exist a million small solutions in java.
In that other subthread, several problems are mentioned that occur even when you use checked exceptions only for exceptional control flow. The big ones that come to my mind are that functional-style methods like .map() and .filter() and the corresponding interfaces like Function<> and Predicate<> cannot deal with checked exceptions at all; the other (related) is that declared exception types cannot use type variables.
With result types, error handling becomes non-exceptional control flow and you can use .filter() and .map() to handle the error cases themselves. With Java's unchecked exceptions, error stay exceptional and the functional-style methods pass them on transparently. Checked exceptions cannot do either, and are incompatible with functional-style methods.
yes, that's the problem ahha
It's unfortunate that Java's checked exceptions do not work in Java's functional APIs like Streams.
I personally find Rust's way of doing things superior (for reasons masklinn mentioned in his comment), but it's basically the same principle, just realised in a different way.
this is how you end up with java checked exceptions where 10% of the codebase is programmer trying to circumvent them and turning errors into silence because of how bad they are
Thanks for providing further resources btw, added Sebastian Witowski presentation to "watch later" list :)
Sebastian's talk is awesome indeed. I learned a lot from it about how Python works without going too much into the implementation details.
That makes it hard to interpret your comment because there are a few places where the article slightly conflates a non-existent key with value == None and maybe you're obliquely referring to one of those, but I'm not sure which one. All of them are OK so long as the dictionary doesn't have having any values equal to None, which it should really spell out but is obviously implied. Actually, most of them even require that none of the values are falsey (e.g. 0, empty list, None, or indeed False).
It would actually be possible to work around that but still fundamentally use the same ideas by using a sentinel value:
# At file scope, not exposed
_not_present_sentinel = object()
# Version B
def read(self, id_: str) -> Item:
result = self.items.get(id_, _not_present_sentinel)
if result is _not_present_sentinel:
raise ItemNotFound(id_)
return resultOptions A) and B) conflate None with non-existent key: if the key is set and has value None, they will return ItemNotFound. Option C) on the other hand treats them separately and will happily return None (instead of raising exception) when the key exists and its value is None.
Here the dictionary is supposed to hold Item values, not Optional[Item], so None would be the signal of a missing item. Think
Lookup k -> Map k a -> Maybe a
Not sure how that came about tho, as the article specifically argues against returning a None for missing entries?In Python, dictionary[key] will raise an exception if the key doesn't exist. If there is a value it's returned, even if that value is None.
The perf arguments are a sham imo.
A callee cannot know how many times it is going to be called, only a caller, or a caller's caller, etc, can know that.
You're adding type hints so they're not going to be blind sided anyways, just return a union of Item and None. If None is a valid value then use a result type.
Exceptions are for exceptional cases isn't just a performance thing, it's a reasoning thing: You're jumping out of the established control flow and there should be a very good reason for that.
If you're struggling with handling None even when you know it's a valid return value, living in a codebase that overuses exceptions is going to be 10x worse.
Python uses exceptions for all sorts of things like to signal the end of an iteration which isn’t exceptional at all. That ship sailed a long time ago.
If the missing item wasn't exceptional they would have just said that.
-
Also if even if it were exceptional, you'd still served extremely well by a Result type.
Instead of just guessing where the landmine is sitting the type system can help you. Python explicitly opted out of type hints for exceptions to avoid a Java-style "checked exceptions" world.
Expected or unexpected, either way there are two paths; I would return a Result/Option type.
def read(self, id_: str) -> Item | None:
...