I never understood the hatred checked exceptions received especially from the younger crowd. I still write Java at work and I still use checked exceptions whenever they indicate an error condition that must not be ignored by the client code. Many new to the project developers hate me for it initially because they can't just "roll out a feature" without being forced to take care of the corner cases but over time they love the discipline that's imposed on them by checked exceptions.
Ergonomics are important. Chaining calls to functions that return a monadic Result/Either type is easier and neater.
Midori nicely separated panics (programmer or system errors) from exceptions. Panics _could not be caught_ - they crashed the process. (Processes were therefore really cheap in Midori). Java, on the other hand, put common programmer errors into the exception category in order to make it easier to work with common code (e. g. `IndexOutOfBounds` should be checked or impossible, but arrays are so common, they decided not to require it and they needed to be backwards compatible so they couldn't make array access return a `Result`-style object).
That said, using checked exceptions well is a great aid to making the code base understandable!
* https://ziglang.org/documentation/master/#Error-Return-Trace... * https://ziglang.org/#A-fresh-take-on-error-handling
I advocate for junior devs to start with the assumption that a new exception should be checked and only consider making it unchecked in specific circumstances.
In Java, a function may throw a long list of checked exceptions, and these lists tend to grow to inconvenient sizes in larger programs. For example, if foo() calls bar() and baz(), which each throw 3 different exception types, then now foo() might throw 6 different exception types. In contrast in Rust, each function can only return 1 error type. If a function needs to represent several different types of internal errors, then the crate that it's in needs to define an error enum type with a variant for each of those. (Either that, or the function can use a generic wrapper error that can contain anything. This is less common in library code but pretty common in application code.) This shifts a lot of work from library callers to library authors, which is a good thing.
Someone with more Java experience will need to correct me here: I think it's fairly common to bulldoze all that complexity by declaring a function that just "throws Exception". That saves you from writing out N different types (and more importantly, from changing every transitive caller when a low level library introduces a new exception type). But it kind of defeats the purpose of checked exceptions, by throwing away all the info they provide. It's a shame that you have this "all or nothing" choice when it comes to exceptions and how much complexity you want to deal with. In contrast in Rust, wrapper errors can define automatic "From" conversions from the lower level error types they wrap, and the standard `?` operator automatically applies those conversions. That means that in many cases, a low level library adding a new error variant might not require any changes in its callers at all. The new information is there for callers who want to look for it, but existing abstractions around the error type generally just keep working.
On the other hand, you're also free to catch and process some and only propagate a subset outside of the function or even wrap the ones you want to process and throw a different exception type from within your function that wraps the existing ones. It's not mandated but pretty idiomatic in Java that any Exception type you define should be able to accept a different exception as its 'cause' in your exception's constructor signature.
The important thing is for the API designer to think about the abstractions, i.e which types of errors should be part of the API and which errors are just implementation details that may change.
If the API designer is too lazy to put enough thought into that then the result will always be what you are ascribing to Java.
I think about this when I think about GraphQL. I like GraphQL, I really do, but GraphQL's N+1 problem is why I don't recommend it. The easy thing is to hammer the heck out of your database, the hard things is to parse the GraphQL request and correctly transform it into a SQL statement. I just don't trust everyone on the dev team to not be lazy.
> I think it's fairly common to bulldoze all that complexity by declaring a function that just "throws Exception".
These are both solved by the same approach: foo should be wrapping those underlying exceptions under its own domain, not just passing them along. This has the added bonus that the further up the call stack you go, generally the more context you have about the operation. So you also get to set a new message that explains that context. So foo should only throw FooException, which might be wrapping an underlying BarException, which might be wrapping an underlying BazException. You get a full accounting of what happened, and how each layer interpreted it.
For example: I invoke the user preferences subsystem to give me the user's foo setting. User preferences can be in a flat file or an embedded database. If I don't wrap exceptions, now my user preference system has to expose that implementation detail by exposing both file and database exception types. Instead I should wrap both of them in a UserPreferenceLoadingException type or whatever makes sense.
I worked with Java professionally for several years and in a sense I feel the same: Checked exceptions are not as bad as they are often portrayed.
At the same time they are certainly not as nice as text book examples make them seem to be. For me the biggest downside always has been that their types are part of the method signature and therefore whenever you change the exception type of one class you more often than not end up refactoring half of your other classes too.
The tension between needing to communicate the actual reason for a failure (which violates abstraction) and preserving the abstraction (which demands shoehorning error states into some kind of polymorphic receptacle) is at the heart of the problem with checked exceptions.
If you try and pass through the failure mode, then new implementations cause new failure modes, which means methods grow new exceptions, which in turn break downstream dependencies, because adding a new exception to the throws clause is a breaking change.
If you try and abstract away the differences in failure mode (e.g. with a specific module exception), then you fill the code with boilerplate wrappers, need to translate exceptions at module boundaries, and (in the worst case) invent taxonomies into which all future implementations must awkwardly categorize their failure modes.
Specific to Java, there's another bug. The type system can express sum types only for concrete exceptions and not for generics. That means that if you parameterize code with other code (e.g. with a lambda or callback), the argument code can only throw pre-defined or runtime exceptions; there's no way to declare, in the type system, the transitive set of exceptions across control flow when the set of exceptions is determined by the argument. E.g. if you have a generic map(f) method, you can't generically declare that map() throws whatever f() throws without forcing f() to only throw a single checked exception - generic type arguments can't be the sum types that Java permits in throws and catch clauses.
All this is costly. Meanwhile, in most actual applications, exception handling is extremely rare; exceptions are almost always propagated up to a top-level handler and logged. When exceptions are handled, it's usually near the leaves of the control flow graph, where the application interacts with other systems, like the network or the file system; these inherently non-deterministic failures often need explicit management. So the work to propagate the exception types throughout the control flow graph is pointless.
And it's just super clunky, but the devs who came up with it just refuse any suggestions to have expressive exceptions (that's what exceptions messages are for). It's impossible to actually know what could go wrong when calling any method.
Here's a random rant online that ends up pitching the more common solution of unchecked exceptions + exception rewrapping: https://phauer.com/2015/checked-exceptions-are-evil/. And of course, in Rust, the rewrapping is basically forced upon you making for a really nice error-handling ecosystem.
I'd say not "younger crowd" but rather developers who have never dealt with a workplace or situation where he/she is demanded to take error-handling (and recovery case) seriously. Different workplaces have different accountability when it comes to code-quality (including error handling and i18n).
Java is more prevalent at some time in the past and it has built an amazing libraries of good/best practices while Python/PHP/JavaScript/Ruby weren't exposed to that level of commercial engineering scale in the past so when the developers came from the latter group, they rebel a bit when it comes to exceptions. Keep in mind that Java was born to address commercial needs thus there is more accountability compare to OSS languages. Just different mindset, different goals hence leading to different design decision.
It sounds like it means “code that is unsafe for a particular reason”; name the reason and leave developers capable of criticising code that is unsafe for other reasons, too.
Unchecked Exception throwing code and stringly typed code are both unsafe. The world is better if we can call spades spades, instead of pitchfork style digging implements, because you want to protect playing card manufacturers.
Perhaps, but if we had come up with a new word, you or someone else would complain that we had done that instead of using a word that people already "understood". :-)
English is a language full of elision, and there's nothing inherently wrong with using one word for closely related meanings. When being precise, Rust uses the term "memory unsafety", but we humans usually shorten that to just "unsafety" because we are lazy.
My point was merely to state that a function throwing an exception [1] doesn't introduce memory unsafety.
---
[1] Notably, we call this "panicking" in Rust-speak; does the fact that we call it something different from "exceptions" confuse people? Probably at least one.
The language provides a way of kinda-sorta recovering from an everything-goes-wrong situation only to be nice to you, and not because that's a thing that's generally possible. You can't always recover from a segfault, or a buffer overrun, or the rest of the kind of things that cause panicking, even if you know that they're theoretically possible in advance, because if you could you would've fixed them already. The best you can do is make your web server return a 500, send logs, ditch its cache, restart in a sane state and hope for the best. You're not recovering that half-finished task, because the programmer can't reason about the program state in a situation where their code has been shown to be wrong.
Panics are only for exceptional failure cases. If this confuses people, it's (probably) because they only thought they understood; they needed confusing.
If you're talking about Rust, I can't comment directly because I haven't used Rust, but I've totally used Go's panic / recover to implement "return" and "break loop" in a language interpreter.
I should really rewrite that thing to do something more sane like bundle up a continuation state to return to, but I was feeling lazy. ;)
If you've got very tightly controlled inputs, it's not unreasonable to expect tightly-bound results (like the last example in the article).
If you're writing intermediary code between unsafe inputs and other libraries, there might be a reasonable boundary to catch all unknown errors and wrap as a failure.
In PhotoStructure's case, it's an easy solution: I simply don't import the given file. There may be a similar reasonable boundary in your problem space.
... but if one is doing that, one should do it very explicitly. I prefer a codebase that separates regular flow of operation from the "catch-all" or "panic-recover" loops that indicate "The buck stops here for catastrophic failure."
Can you expand a bit on how you do this internationalization? I've got a low-priority issue [1] to investigate how to integrate SNAFU and Fluent to get i18n for error messages, but knowing how someone does it today would be super helpful!
Internationalization of error message? I didn't know that was a thing!
I'm curious about that, do you have some examples I could check?