And ones that are so unergonomic that only industrious developers will get them right will result in worse software.
Modern languages allow these to be zero cost abstractions so there is little tradeoff.
For instance the IO monad, which is used everywhere Scala/Cats. It can contain a result or an error. If you don't feel like checking for an error after you called some method, you can just pass it up and return the IO from your method. Does that sound familiar? It behaves just like a checked exception, the only difference is that methods don't need to declare any error or exception in their IO signature.
Then it behaves like an unchecked exception. Which is fine.
'Checking' is the act of modifying your source code to declare the error.
No. If I add a checked UserNotFound exception to a getUser db call, you can bet someone higher up the stack will do try catch Exception e, so now they're catching OutOfMemory and who knows what else.
come on, it's not as if anyone disagrees with that, but that's extremely, extremely rare, overwhelmingly, callers are just dealing things like with UserNotFoundException, NullPointerException, what have you, and there's no reason why the compiler should happily let you catch Exception (or nothing) when it could just be giving you an honest object back
But that's laziness on the caller's part. If I offer a method but the caller decides to do reckless lazy crap with it, there are many different ways to get there in any language. I typically call those out (Exception e) at code reviews.
There are countless examples of rare but legit non-obscure use-cases. And even if your code is fine, you can't expect the same for the libraries you are using.
And some exceptions make frequent non-happy paths more visible. Most of all IOException, because IO can _always_ fail for all the wrong reasons (because the failing of this exception is outside of the JVM's influence, it is rightfully a checked exception). And often you simply don't want to do the error handling at call-site but propagate to the code which is controlling the use-case.
But more importantly, force-unwrapping is not equivalent to catching generic exceptions. Instead, it's equivalent to catching all checked exceptions and wrapping them in a Runtime error. It's also almost equivalent to what this compiler plugin does (or Kotlin or Lombok's @SneakyThrows do).
Catching "Exception" and trying to handle it generically, is more closely equivalent to this type of code:
match result {
Ok(value) => doSomethingWithValue(value)
Err(e) => println("Error: {e}!")
}It’s got nothing to do with getting better. It IS basic Java exception handling. Any proper course or tutorial will tell you to catch specific exceptions
any real world library or application will catch Exception e, catch checked exceptions and turn them into unchecked ones etc etc
I hate checked exceptions when they force me to handle an exception which I know is impossible given the arguments passed to the method. For example, some older Java APIs take an encoding name and force you to handle the checked UnsupportedEncodingException - even when the encoding name was something like “US-ASCII” or “UTF-8” which is required to exist by the Java standard, and if somehow they didn’t there is often no possible error recovery than just crashing. This has been fixed in newer versions by introducing new APIs which throw an unchecked exception instead, and also by introducing constant objects for the standard charsets
If I want to ignore a set of exceptions, I have the option to catch(Exception e) {} signaling that I recognize the risks that have been explicitly communicated by the API (that throws). An @IgnoreExceptions annotation would help dump the 5? boilerplate lines.
The unknown risks for other non-specific Runtime exceptions, are not included. I can catch those too if I add a catch(RuntimeException e){}, again signalizing that I recognize the risks such that other developers understand that I'm opting out of handling those conditions, which may or may not be errors in a classic sense. eg an expected socket not being available causing an IOException, because I'm doing some concurrent process.
Without checked exceptions, you end up with far fewer catch clauses, and hence far fewer opportunities for buggy catch clauses. I think in most apps, most of the time, you just want to bubble all exceptions up to a central point which handles them – for example, in a web server, you can have a single catch clause for the whole HTTP request. If need be, you can make that central point extensible with custom handling with specific exceptions (like JAX-RS ExceptionMapper, or @ExceptionHandler beans in Spring – which you can then inject using IoC/DI). Once you've got that working, if you need catch clauses deeper in the code (e.g to return an error code to the frontend when the client sends bad data instead of just failing the request with a `NumberFormatException`), you can add them where appropriate – but unlike checked exceptions, you aren't forced to add them where you don't need them.
I've seen quite a few in for-loops, so I'm not sure that's tracking for me.
> Why? Because they encourage you to put catch clauses everywhere
Java forces you to handle author-specified error conditions (CS error), but jr developers do tend to create their own more often than necessary.
While I agree that a global exception handler is good practice for Spring applications, individual exception handling is very common and important to modern software. eg If Web APIs get more complex (multiple datastores), you find you don't want to bubble everything up. I get a request, want to check a cache (which might throw) then look it up in a DB (which might throw) then look it up in a file (etc), then return a response.
I do wish I could handle exceptions simpler than making the choice to add 4 lines (+ any actual logging, etc) or blackhole-traveling through the stack to add checked exception signatures everywhere (code AND tests).
The difference is that for-loops are almost an essential language feature – the vast majority of languages have them (or a close equivalent), and they make certain algorithms a lot easier to state clearly. Sure, there are some languages which lack them, but they tend to be either languages with non-mainstream paradigms (such as pure functional or logic programming languages) which put all the emphasis on recursion instead, or else really old legacy languages which predate the development of structured programming (such as pre-1980s versions of COBOL–modern COBOL versions have for loops)
By contrast, almost nobody considers checked exceptions an "essential language feature" – languages which lack them vastly outnumber languages which possess them, indeed, Java stands out as the only major mainstream language to possess them
Given the argument "this unusual inessential language feature causes more bugs than it prevents", the response "this essential language feature which the vast majority of languages have sometimes causes bugs too" isn't very convincing
Good developers should be lazy. Checked exceptions require you to do a bunch of cumbersome ceremony that makes your code unreadable, for what should be (and is, with Either or equivalent) at most a couple of symbols; no wonder developers hate that.
> I would also argue that checked exceptions are no more complex than Eithers, Try or Applicatives.
You'd be wrong. Checked exceptions as implemented in Java require two keywords of their own (throw and catch), a special change to method signatures (throws), a unique new kind of expression for multi-catch (|), a unique new kind of type for things caught in multi-catches, and special support in every other new language feature (e.g. Futures have a whole bunch of extra code to deal with exceptions). Eithers can literally be a regular class that you could write yourself using ordinary language features. A small piece of syntax sugar (something like "do notation", "for/yield comprehensions", or "?") is a good idea, but not essential, and if you do it right then you can implement that once across your whole language (for applicatives/monads in general) and use it for eithers, futures, database transactions, audit logs, almost anything. https://philipnilsson.github.io/Badness10k/escaping-hell-wit... .
try {
throws_a();
r = grab_resource();
throws_b();
r.throws_a();
} catch (a) {
r.release(); // oops; null pointer some times
} catch (b) {
try {
r.release();
} catch (a) {
// nooo....
}
} finally {
if r != null {
r.finalize() // use after release (sometimes)
r.release() // ???
}
}
There is plenty of academic literature showing that real programs are even worse than my contrived example, on average.Quite sure Java would prevent that if r was never assigned.
Also most languages provide cleaner try-with-resources or RAII style lifetime management so you don't actually end up with that kind of spaghetti code unless you actively go out of your way to be bad at programming.
Context managers (try with resources) specifically don't work at all when release is conditional e.g. open a file, need some post processing, need to close the file if the post-processing fails but return it if it succeeds.
Defers don't either, unless you have a specialised one (e.g. zig's errdefer).
Actual RAII (aka affine types) does work, but adding that to a GC'd langage after the fact is a huge undertaking and increase in complexity, because the interaction between affine and normal types is fraught.
The paths are still there if the exceptions are all unchecked or combined or not using exceptions at all. Passing exceptions up is rarely the right choice, imo. If it's a terminal runtime, sure, obv.