Exception based error handling is unsafe when they are unchecked exceptions. Checked exceptions however are as safe as Either, Try, Monads, Applicatives or whatever.
You are forced to declare them in your method signature, the caller is forced to either handle them or rethrow them + declare them as well.
And I guess this is precisely why so many developers hate them; they don't like the extra work they have to do to catch all those edge conditions.
This is why we see so many empty catch blocks or upcasting to Exception or even Throwable; it is laziness.
I would also argue that checked exceptions are no more complex than Eithers, Try or Applicatives. Actually passing Eithers or Applicatives around everywhere can easily clutter your code as well, IMO it can be worse than checked Exceptions.
> And I guess this is precisely why so many developers hate them; they don't like the extra work they have to do to catch all those edge conditions. This is why we see so many empty catch blocks or upcasting to Exception or even Throwable; it is laziness.
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... .
The construction of arbitrary convolutions of logic is not demonstrative of your assertions. A multitude of code paths is not specific to checked exceptions. Note: It is possible to try catch a union of exception types in the recent versions of Java (catch X | Y | Z).
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.
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.
> 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.
I have been coding in Scala/ Haskell for the last 10 years, before that 15 years in Java. What I'm seeing is that there are as many lazy developers in FP as there are in Java, maybe even more.
And despite all the nice safeguards that FP provides, there are still plenty of ways for lazy devs to work around them.
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.
> And I guess this is precisely why so many developers hate them; they don't like the extra work they have to do to catch all those edge conditions
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
> I hate checked exceptions when they force me to handle an exception which I know is impossible given the arguments passed to the method.
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.
I've seen checked exceptions cause numerous bugs. Why? Because they encourage you to put catch clauses everywhere, and a lot of developers don't know how to write them properly (and even the best developers sometimes make mistakes that slip through the cracks). A common result of this is that a bug causes an exception, but the catch clause loses information about the original exception (such as by logging it without the stack trace, or throwing a new exception without setting the cause). Or else the catch clause just ignores the exception, and then you get some other error later (e.g. NPE because some field was unexpectedly null), but again you've lost the info on what the root cause problem is. If your code base contains 100s of catch clauses, that's 100s of opportunities for buggy catch clauses to exist. And even if you eradicate them all from your own code base, then you might get hit by one buried in a third party library.
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 checked exceptions cause numerous bugs.
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).
> I've seen quite a few in for-loops, so I'm not sure that's tracking for me.
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
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.
> you can bet someone higher up the stack will do try catch Exception e
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.
Force-unwrapping a Result type is something you can do in multi-paradigm languages such as Rust, but not in stricter functional languages like Haskell - at least not easily (and we're worried about developers taking the easy way out here).
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}!")
}
What about DivisionByZero, NumberFormatexception, ArrayStoreException?
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.
You can choose to unwrap the result type wherever you want, as opposed to it automatically unwrapping in place at the call site and having a weird propensity to encourage a "special" return from there.
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
This is the most common reply I see whenever anyone proposes a safer, better way of coding, and it's not a good one. "Just get better" like oh ok except that in the real world people are gonna people and even the best programmers in the world make mistakes and do dumb, lazy shit. Our tools should be designed such that the safest, most correct way to do anything has the path of least the resistance. Not happily allow you to ignore exceptions, or catch less or more exceptions than required. Functional error handling corrects this, exceptions do not. Anyway, it's clear we're not going to agree, and you win, since so far the industry is still stubbornly clinging to exceptions, despite them being a failed feature in every language they're in.
It’s not even lazy, your IDE will not do this for you by default.
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
I would also argue that checked exceptions are no more complex than Eithers, Try or Applicatives. Actually passing Eithers or Applicatives around everywhere can easily clutter your code as well, IMO it can be worse than checked Exceptions.