8 ms·
Exceptions are an anti-pattern. If a function can encounter an error, it should expose this fact through its interface (specifically its return type). For exam
by peterkelly 4y ago
Exceptions are an anti-pattern. If a function can encounter an error, it should expose this fact through its interface (specifically its return type).
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.
[1] https://doc.rust-lang.org/std/result/index.html https://doc.rust-lang.org/std/result/index.html
- bhawks 4y agoRust also has a much more rigorous type system than other languages - so yes in rust exceptions should be considered an antipattern. 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.
- moring 4y agoEven Rust has panic for things like out-of-bounds access. In fact, Rust's panic / Result is actually very similar technically to Java's checked / unchecked exceptions. I'd argue that this is more a difference in usage and culture, with likely influence from the fact that Java makes it easy to catch unchecked exceptions, so many prefer unchecked exceptions to avoid being forced to think about what to do in case of an error (which, of course, means that probably the wrong thing happens then). 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).
- masklinn 4y ago> I'd argue that this is more a difference in usage and culture 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.
- girvo 4y agoNim does “checked exceptions”-ish via its effects system and {.raises: […].} pragma. Though it’s not just limited to exceptions. Can be used for all sorts of useful things!
- codeflo 4y agoTo add to that, Java doesn’t make it convenient to have a place that catches checked exceptions only, because the inheritance hierarchy is the wrong way around for that. 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.
- grogers 4y agoIf you are looking to catch only checked exceptions of unknown type, it's easy enough to first catch and rethrow Error and RuntimeException, and then catch Exception and do whatever. The hierarchy is garbage, but at least it's fixed in stone. Not super useful though if you know the exception types.
- peterkelly 4y agomasklinn already made lots of good points. I'd add that panics in Rust are for intended for non-recoverable errors (like memory allocation failures, out-of-bounds access) and under most use cases are equivalent to abort(). There are mechanisms to catch panics in Rust, but the docs state: "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 https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
- pjmlp 4y agoOne should also note that while Java famously takes the blame, checked exceptions appeared in CLU, were adopted by Modula-2+, Modula-3 and C++, before landing into Java's design. 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.
- derefr 4y agoTo me, the key problem with Java’s error handling is that there’s no special consideration given to functional interfaces like Callable/Runnable/etc — so you effectively cannot throw a checked exception from within a lexical closure passed to a Stream.map, ExecutorService.submit, etc: trying to do so gets you a compile error. 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.)
- asplake 4y agoA language antipattern maybe, a programming antipattern, no. Python isn’t ML – it got pattern matching only very recently. Without it, and in a language where type tests are often a bad smell, exceptions are perfectly reasonable.
- kazinator 4y agoA machine language instruction like 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?
- moring 4y agoLoading a not-present page is typically handled transparently by the OS. Exposing it to the userspace program isn't really useful. 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.
- kazinator 4y agoLoading a not-present page is handled by an exception being signaled, which the operating system has to handle with code. 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.
- jayd16 4y agoHow can you argue that a result type is more forcefully thought about than checked exceptions? 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.
- valenterry 4y agoChecked exceptions like in Java have terrible ergonomics and require a lot of special handling by the language and don't work well in combimation with other language features. Return values also have their cavets, but I think languages like Rust are doing well enough so that their ergonomics are way above what you can get with check exceptions, even with a good implementation.
- rhn_mk1 4y agoSo you mean you advocate for checked exceptions? Like Java's "throws" keyword? https://www.baeldung.com/java-checked-unchecked-exceptions https://www.baeldung.com/java-checked-unchecked-exceptions
- moring 4y agoI raised the same objection in another subthread and got reminded how Java's checked exception, while attacking the same problem as Rust's Result<> type, are really abysmal from a usability POV. Now I remember why I never used them...
- fisf 4y agoChecked Exceptions are really not worse than error result types unless you abuse them for normal control flow. Normally, Rusts exception handling is almost exactly isomorphic to checked exceptions in Java. For general results cases (i.e. non exceptional cases), there exist a million small solutions in java.
- moring 4y agoThe problem with that is that checked exceptions ask you to be treated as normal (non-exceptional) control flow. Hence the appeal of unchecked exceptions because they allow you to treat exceptional control flow as exceptional. The comparison of checked exceptions with result types is because they are competing solutions for normal control flow. 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.
- 4y ago
- gpderetta 4y agoSure you you start with an error monad, then you add do notation and then you end up with (checked) exception syntax and semantics. Then the cycle repeats. 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.
- randyrand 4y ago1000% 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.
- quietbritishjim 4y agoThere are plenty of code bases that are dramatically simpler when exceptions are in use, especially if you just want to read and understand the happy path. I'm tempted to give an example or two but I'm sure most readers know that really. 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.
- jcelerier 4y ago> Exceptions are an anti-pattern. If a function can encounter an error, it should expose this fact through its interface (specifically its return type). 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