Checked exceptions are far and away the worst part of java and I'm glad that no other language I've personally encountered have them!
On a more real note, I work at a shop with a fair amount of java now and checked exceptions are definitely my biggest complaint. I hope we adopt this, I'm sure we won't, but I'm glad to see a little movement on what I feel is the crusty status quo in the java world.
Especially the higher the level the code is the more potential lower layers that can fail. It's just not scalable to write code up front to deal with every single case. And most programmers don't care about failure, or working around it, just success.
On the other hand we do expect the unexpected and wrap failure prone code in try/catch so that we can log exceptions and keep the app moving. Figure out later if the ROI is there from enough failures to write special code to try to prevent and/or recover from a particular exception.
It's pretty much the same as it is in the checked exception world just with more code - in Java people handle the exception, log it and/or rethrow, so what's the point? Idealism and poor assumptions by the language designer. Anyone have any proof that a Java app is more reliable than a C# one?
Vast majority of the time either the caller will fix it, or it simply does not need to be fixed at all it's simply passed all the way up to the top.
The type-safe JSON feature in Manifold could be incredibly useful.
More on checked vs unchecked exceptions here: https://forum.dlang.org/thread/hxhjcchsulqejwxywfbn@forum.dl...
try {
theUpdatedFunction();
} catch (MyNewCheckedException e) {
logger.warn("Whoopsie doopsie", e)
throw new SomeUncheckedException("Something failed, idk", e)
}
Which really is a zero sum game. The code still breaks the same way, but the checked exception gets eventually wrapped in an unchecked one. We still would have the situation that someone changed the behavior of the function in a way that is incompatible with your usage.That bug should have been caught by tests, code reviews and good communication.
> You see programmers picking up new APIs that have all these throws clauses, and then you see how convoluted their code gets, and you realize the checked exceptions aren't helping them any.
And he goes on to elaborate on how this gets more complicated when versioning is introduced into the mix.
There's a reason we have compile-time checks. If you think compile-time checks should be replaced with additional tests, code reviews and good communication, then you want a scripting language, not a compiled language.
You do not wrap checked exceptions in an unchecked one... unless you are a really bad Java programmer.
try {
theUpdatedFunction();
} catch (MyNewCheckedException e) {
logger.warn("Whoopsie doopsie", e);
throw new SomeUncheckedException(e.message);
}
There's a special place in Hell for these people, but at least they'll get to see all of their friends again.That's not a valid argument. This is an explicit decision, so they reap what they sowed.
One could just as well make the same argument for Optionals, that you can just do:
let val = foo.unwrap();This is such an unproductive cop out response. The problem could be completely solved by functional error handling, which leverages the compiler to force you to handle exactly the errors than can happen, no more, no less.
Who does that? It looks like an anti-pattern.
I couldn't disagree more. Checked exceptions in Java have ruined a generation of programmers.
The truth is, under checked exceptions, to satisfy the compiler the function that you called would declare that it throws a SomeModuleException and the programmer who wrote that function would put all his code in try/catch block that catches all errors and rethrows a SomeModuleException with the real exception passed as the cause.
Checked exceptions just don't work.
I have seen libraries that make network calls that do the following:
* Throw an exception on certain types of network errors * Return an HTTP error code for other types of network errors * Return an object with error set to true and an error string for other types of network errors
NONE of those was documented, it is crazy. This actually bite me in prod when an undocumented exception that we hadn't seen in over a year of uptime was finally thrown. I had to look at network logs to try and figure out why it was being thrown (no checked exceptions in JS!), which itself is absurd.
https://learn.microsoft.com/en-us/dotnet/api/system.exceptio...
However in small to mid sized enterprise software companies with average developer talent it’s important to keep boundaries (and blame) clear.
In the scenario in question, the CTO will blame OP and make them work the weekend to diagnose/fix it, so wrapping the other exception and throwing their own SomeModuleException will cover their ass.
Java is popular for all the wrong reasons. Believe it or not there are hundreds of such companies in the US alone heavily using Java in this manner and then there is the whole offshore development segment.
There is no magic language trick that can prevent you from having to rerun all tests for your software if you update a dependency. Pretty much period.
(I say this as someone that isn't really opposed to checked exceptions.)
Of course, but having a checked exception (even better, having the errors be part of the return type via Result<T, E> or Option<T>) solved an entire class of problem. Doesn't mean that there aren't others of course. But surely this is a win?
>inb4 it makes the code very complex with nested return type Result<Result<Result<T, FileNotFoundError>,ReadError>, ParseError>
Then it was just hidden from you before. The complexity had always been there, it just never occurred to you that it can happen.
Hopefully, your answer is compile time. If so can you now understand why you would want to be alerted about a new exception getting thrown at compile-time as well?
Not necessarily. If it started throwing a new RuntimeException, it wouldn’t have. There are also sneaky ways to throw checked exceptions without declaring them, for example using Lombok’s @SneakyThrows annotation
Functional error handling types are much simpler, safer and more powerful.
Simpler because they don't rely on dedicated syntax- they're just regular objects no different to any other object.
Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling)
Powerful because they support map, flatmap, applicative etc, making it easy to eg chain multiple computations together in desired ways, which is unwieldy and bug prone when using exceptions.
> What is wrong about dedicated syntax
It adds complexity to the language! It could be that, when learning Java, Kotlin and any other language, we learn that methods return what they say they do... and that's that. No weird dedicated syntax and magic, special treatment for returning anything other than the happy path, and the HUGE complexity that comes with it, eg the dedicated syntax itself and how it behaves, differences between checked and unchecked exceptions, hierarchies of exceptions etc etc.
> Exceptions are easier
But that's the point, they're not.
Exceptions based error handling is unnecessary, hugely complex, doesn't compose at all, obfuscates or straight up hides what can go wrong with any given call, so leads to countless trivially preventable bugs... I could go on. And after decades of use, there's still no consensus about what exceptions should be or how they should be used. Exceptions are a failed experiment and I have no doubt that in ten years, Java, Kotlin and many other languages will acknowledge as much and move away from it the same way Joda Time outcompeted and replaced the horrible Java date and time library.
That's why Stream::map can not capture the checked exceptions properly.
I'm sure I'd get used to it eventually, but I like that unchecked exceptions in Java are now an option!
https://www.artima.com/articles/the-trouble-with-checked-exc...
The fact that Java has introduced UncheckedIOException, in my opinion, shows how some people in the Java community have come to believe that checked exceptions were a mistake (understanding that lambda forced the issue). There's probably not too much to be easily done at this point, but consideration for changing checked exceptions in the JDK to extend RuntimeException sure would be interesting.
https://github.com/rogerkeays/unchecked/blob/f22c8cde3557de0...
No need to change the type hierarchy. Just make it a compiler option.
https://github.com/manifold-systems/manifold/tree/master/man...
With an article having the same name as your post:
https://github.com/manifold-systems/manifold/blob/master/doc...
This tool does remind us that checked exceptions in Java are a compile-time phenomenon only, so (non-compliant) compilers can be made to ignore them. Neat, but... helpful? It would certainly lock you into using the tool.
Instead, we need configurable policies for error handling, where the same code can be statically analyzed much more deeply (e.g., to include unchecked exceptions) or run more leniently (e.g., when exceptions are used only for flow control). Some of this might pertain to local declarations, and some might be policies of various scopes (per VM, per-package, or per call-chain), and would probably fold in assertions and fibers/virtual threads and some OS-sensitive behaviors. The goal would be to write the code once (with full disclosure, i.e., only check-able exceptions and assertions), but to reuse the same code under different policies.
Whether anyone wants to do the work of folding this into the VM as security privileges were is another question...
- It requires modifying compiler arguments and putting some file in the local classpath! That's really not Java-ic. Is it?
- It is not transparent in the code. You cannot infer by looking at the source code that something has changed.
Since some time ago I use the NoException library[0] in my Java projects which achieves the same goal but without the above-mentioned issues. It can also be used to mimic Scala's Try construct.
As for alternatives, Try/Result and similar monads have decent adoption even in Java, but personally I quite like the Kotlin philosophy [1] to not have generic error containers and either use runtime exceptions for errors that cannot be reasonably handled by the caller and encapsulate failures in the return type if they can.
[1] https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/...
TIL!
What I don't like is the examples of having to use Maven or Gradle to install it. Why does that have to be so verbose, and something written in another language?
[EDIT] I moved the command line stuff back to the top of the README. XML makes my eyes bleed too...
As you correctly mentioned, if you leave the compile-time type of a variable unchanged, then assign a different object (one that belongs to a subclass that doesn’t throw the exception when you call its method) to that variable, and then call that same method, the compiler still forces you to catch. That’s the Liskov substitution.
This exception is a {@link RuntimeException} because it is exposed to the client. Using a {@link RuntimeException} avoids bad coding practices on the client side where they catch the exception and do nothing. It is often the case that you want to blow up if there is a parsing error (i.e. often clients do not know how to recover from a {@link JsonParseException}.
I have *completely* the opposite style: Throw checked for anything that can throw, except: null pointers and invalid arguments (e.g., negative port number). Why so much checked? The caller cannot hide from it. They are forced to ack it, else compiler error. At the very worst, they are free to rethrow as a wrapped RuntimeException (ugh).When I write code in C#, I always left wondering: Does this method throw an exception (ignoring null pointer and invalid arguments)? It bothers me. And, I write that as someone who really likes C# -- it's a great language for my needs.
And, yes, I can appreciate that many people don't like the friction associated with checked exceptions. It does add a lot of visual bloat! Serious question: What are the alternatives that _communicate sufficiently_ through an interface? Honestly, I struggle to think of a better alternative, so I just live with it.
Just like the bad old days of integer return values used to indicate error state (C-like languages), you need to check every single call -- there are no shortcuts, especially with I/O. For me, it is similar with Java and C# when writing I/O code: You need to catch and correctly handle every (checked-ish) exception.
Also, without IDE support such a plugin will never gain a lot of traction.
https://github.com/manifold-systems/manifold/tree/master/man...
I do just want to correct a tiny piece of the readme:
> a common practise is to rethrow it as a RuntimeException. The problem with this, ... is that the root cause of exceptions get hidden,
This is most definitely not true. If you wrap / re-throw as a RuntimeException you will absolutely get your original stack trace.
That said, patching compiler is not something I would do.
What I usually do is add unchecked exception class for every checked exception I had to "handle". So I have UncheckedIOException from standard library (handy!), UncheckedSQLException and so on and so on.
Yes, it causes more code, but Java is not exactly known for brevity, so be it. At least that way is not hacky in any way and actually used by Java standard library itself, so I'd consider it "idiomatic".
public static RuntimeException unchecked(Exception e) {
Exceptions.<RuntimeException>throw_checked(e);
return null;
}
private static <E extends Exception> void throw_checked(Exception e) throws E {
throw (E) e;
}
then you can do this: try { ...
} catch (IOException e) {
throw unchecked(e);
}
And the original exception is thrown without being wrapped. It's as though you had `throws IOException` on your method signature.This is from my original solution to the problem: https://github.com/rogerkeays/jamaica-core/blob/0cc98b114998...
So yes, you can throw IOException masking it to RuntimeException, however you can't catch it later without another gimmicks like catching Exception and checking if it's IOException (and even that check causes linter warnings in Idea, so now you need to suppress those warnings...).
Throwing and catching UncheckedIOException does not have this problem.
There was maybe a time Lombok made sense. It does not anymore. Death to Lombok.
I'd like something to soften some of them, probably in a configurable way, in some builtin interfaces btw. E.g. many IOExceptions should really be unchecked.
Note: I will try it, because maybe I'm wrong.
In my book, if anything, they are much much better! Unfortunately they don’t have a flawless implementation, but hopefully languages with first-class effects will change that.
No, they aren't.
They are not compositional. You may want to write `f(g())` but there's no way to write the parameter type of `f` to make this work (in Java). That's because checked exceptions are an "effect" that would require extending the Java type system.
In languages that rely on Result/Either for error handling, you've got two types of errors: Typed errors (Result/Either) and untyped panics. Typed errors are supposed to be handled, possibly based on their type, while panics can be recovered from ("catched") but these are serious, unexpected errors and you're not supposed to try to handle them based on their type. Since typed errors generally need to be handled explicitly while untyped errors are unexpected, typed errors are always checked (you can't skip handling them), while untyped errors are unchecked (implicitly propagated up the stack if you don't do anything to catch them).
Java has three types of errors:
1. Checked errors, a.k.a. checked exceptions: (exceptions that inherit from Exception, but not from RuntimeException). 2. Unchecked application errors: exceptions that inherit from RuntimeException. 3. Unchecked fatal errors: exceptions that inherit from Error.
These three kinds of errors live in a confusing class hierarchy, with Throwable covering all of them and unchecked application errors being a special case of checked application errors.
Like everything else designed in the early Java days, it shows an unhealthy obsession with deep class hierarchies (and gratuitous mutability, check out initCause()!). And this is what destroyed the utility of checked exceptions in Java in my opinion.
Consider the following example: We have a purchase() function which can return one of the following errors:
- InsufficientAccountBalance - InvalidPaymentMethod - TransactionBlocked - ServerError - etc.
You want to handle InsufficientAccountBalance by automatically topping up the user's balance if they have auto top-up configured, so you're going to have to catch this error, while letting the rest of the errors propagate up the stack, so an error message could be displayed to the user.
In Rust, you would do something like this:
account.purchase(request).map_err(|err| match err {
PurchaseError.InsufficientAccountBalance(available, required) => {
account.auto_top_up(required - available)?
account.purchase(request)
}
_ => err // Do not handle other error, just let them propagate
})
In Java, you would generally do the following: try {
account.purchase(request);
} catch (InsufficientAccountBalance e) {
account.auto_top_up(e.requiredAmount - e.availableAmount);
account.purchase(request);
} catch (Exception e) {
// We need to catch and wrap all other checked exception types here
// or the compiler would fail
throw new WrappedPurchaseException(e);
}
The "catch (Exception e)" clause doesn't just catch checked exceptions now - it catches every type of exception, and it has to wrap it in another type! Of course, you can also specify every kind of checked exception explicitly, but this is way too tedious and what you get in practice is that most code will just catch a generic Exception (or worse - Throwable!) and wrap that exception or handle it the same way, regardless if it was a NullPointerException caused by a bug in code, an invalid credit card number.The worst problem of all is that once developers get used to write "catch (Exception e)" everywhere, they start doubting the values of checked exceptions: after all, most of their try clauses seem to have a generic "catch (Exception e)", so does it really matter at all of they're using checked exceptions?
This is the reality. Checked exceptions failed in Java. Most Java developers see them as nothing more than a nuisance and look for ways to bypass them. That does not necessarily mean that the concept of checked exception as a language level facility for errors has failed, but it certainly failed the way it has been implemented in Java.
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.
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.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.
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
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.
Which is bad. In 99% of cases I have no specific error handling for the given problem. Just let the process crash or be handled by application server / framework.
Representing these kinds of error conditions in the code which I have no interest in handling is just noise.
> I have no specific error handling for the given problem
then pass it on and decide what to do with it at the system boundary
in the db:
fun getUser(uuid: UUID) : Either<Error, User>
middle layers: pass around the either, you can map, flatmap etc on it to chain it with other computations there
then in the resource layer
return user .fold({ error -> when(error) { is DbError -> HttpResponse.InternalServerError() is UserNotFoundError -> HttpResponse.NotFound } }, { user -> HttpResponse.ok(user) })
Then, at every layer, each method explicitly says in the method signature what it returns. There’s no need to look around each and every line of every method in every layer, or in the framework, or some global exception handler, to figure out what will happen. Developers can tell from a glance at the resource method what the endpoint will return for each outcome, in context.
Things like being unable to communicate with the DB or not finding a user are not exceptional, they're entirely expectable when you're calling a db to look for a user and should be modeled accordingly.
That said, not modelling them in the type system is a mistake. But the model has to be useful - knowing what functions can or cannot throw is useful, knowing what they throw, less so.
(They are safe because of try-finally / try-with-resources)
That's what wrong with them. Callers, in almost all cases, do not know what to do exceptional outcomes. So you force programmers into a situation where they're dealing with things they shouldn't be dealing with.
If it's some random web backend, it's often fine to just let the error propagate as a 5xx. Many error cases wouldn't have a better solution anyway, and all that's breaking is a single page or resource load. (Of course on the frontend it might be nicer to show some kind of an error than just e.g. having frontend functionality silently fail.)
If it's a desktop application and you're crashing the entire application because wifi happened to drop or whenever there's anything your happy path didn't predict, that's going to be bad.
If it's some kind of an embedded thing and you're crashing an entire device because you didn't bother to handle cases that are outside of the happy path but entirely possible, I only hope it's not something important you're developing.
What and where you can handle exceptions has nothing to do with where the exception is thrown. Handlers only exist at key points in the application at the start of operations where you can skip, abort, or retry.
Of course everyone like coding like this. No one likes to code error handling code.
Just like no one likes to code tests and documentations.
It turns out not every important thing is joyful.
I honestly think the best error handling strategy is employed by Zig, then Rust. they're very explicit while not getting in the way, you always know what throws what and what doesn't.
Rust has a little issue though, which is that people can make their functions return `Result<T, Box<dyn Error>>` which makes handling the returned error difficult, Zig does not have any of that.
Take for example if you call to an OS API and it returns an error that wasn't documented to be returned from that function. You can't return that code up the callstack using Zig's error mechanism. Instead there's functions such as unexpectedErrno and unexpectedError. Those call the appropriate method to get a string representation of the error and call std.debug.print to display it, then they just return error.Unexpected.
That means that the caller doesn't have any control over how the error is displayed. Meanwhile in Rust, you can add an UnexpectedError variant to your enum and let it carry an error code with it. The caller can then display that error however they want.
I don't hate Zig's error handling by any means, but personally I think Rust does it better. I'm happy to see that C++ seems to be going with the Rust way by adding a Result-like type std::expected.
But I don't think C#, Kotlin or Scala are less safe than Java even if they do not have the concept of checked exceptions.
>Makes it impossible to call a function safely ?? catch(Exception e) anyone?
[1] https://literatejava.com/exceptions/checked-exceptions-javas...
[0] https://www.yegor256.com/2015/07/28/checked-vs-unchecked-exc...
I have a desktop application with a single exception handler wrapping it's event loop. The handler display the message to the user and returns the to loop. It's rock solid -- nothing can crash it. The user can try to save their file to some network share that just disappeared and when that fails just save again somewhere else.
But there is no such thing a safe function to call. Whether you handle errors by meticulously passing them around everywhere or not makes little difference.
Instead of the exception type being checked or unchecked, the throw should specify checked or unchecked.
So for example the first time the exception happens it can throw a checked exception for the caller to deal with.
If the caller doesn't deal with it it can simply throw its way all the way to the top without every single function needing to declare they handle it.
try {
somethingImportant();
} catch (Exception e) {
// can't happen
}
IJ finally started chirping about this other anti-pattern try {
doit();
} catch (Exception e) {
System.err.println("something didn't work");
// failure to reference "e" is now a yellowbox
}
I have to constantly ask for them to either log it, or rename the variable to `ignored` to get IJ and future readers on the same page that it was intentionally swallowedNot when it creates more issues than it solves, which checked exceptions do.
Checked exceptions are an entire side channel to the type system which breaks any sort of composition or genericity.
Maybe this is solvable, but Java seriously poisoned that well because its implementation is so shit, and if you’re looking for this static safety, first the rest of a Java-style type system does not justify it (there’s so many low hanging fruits), and second a result-style things will already give you the same benefits in a form which is known to work.
Signals that don't unwind can have their own problems, of course. I don't mean that as a silver bullet. But all too often the exceptions and error conditions that we use to teach these ideas are far more difficult because of our insistence on unwinding the stack. Neither return values nor exceptions change that.
I've been coding in Java professionally for ~20 years. I can count with zero hands the number of times I've been burned by a getter or setter getting changed into something surprising.
If you really need auto-generated getters/setters/builders - Immutables [1] is a library that does it using bog standard annotation processing rules that don't require hacking your build process.
I’ve seen too much code were some random problem in an Error - OutOfMemoryError for example when processing too much data (Eg call data records, payment records, analytic records, whatever). If it’s a batch processing job, you don’t want this problem for this specific entity causing the rest of your reports not to be sent.
I’ve also seen stupid things like RPC libraries silently swallow Errors and not report them properly to the caller, so we end up wrapping all RPC server endpoint methods in a try-catch-Throwable just so we can see the problem and log it.
As such, any breakage from calling would be, by definition, a bug. And no, we have not found a way to prevent bugs.
The library code can already throw anything. OutOfHeap, over/underflow, div0, stackoverflow, threadinterrupted. The caller already knows the function can throw, and documenting one more flavour of throw doesn't tell the caller anything.
but it does, in cases when there are errors that can or should be retried. Like a whitelist of documented cases where recovery and retry are possible, and of course all this infinite runtime stuff that can happen unexpectedly, for which there is no immediate solution.
If I’m processing a monthly report for millions of customers, I don’t want to abort processing after running into one problem customer. I want to continue to process the rest of them, and log the problem customers exception for troubleshooting and analysis offline.
That was an issue right from the start, and it only got worse as the type system got richer.
would you elaborate this statement?
In a similar vein, it's ironic how often you hear "composition is better than inheritance" in OO design context, and yet how few OO languages have facilities to automate delegation.
I wholly agree with this sentiment. Rust Result types where also excruciatingly inconvenient to use at the early days, but Rust gradually added facilities to make it better: first the try! macro and if-let, then the ? operator and finally let else. Together with crates like anyhow, thiserror and eyre, error handling became a lot better. I don't use Swift a lot, but it also seem to have iterated on its error handling.
In the 27 years of its existence, Java did very little to improve exception handling facilities. It added exception chaining in Java 1.4 and catching multiple exceptions in Java 7, that's it. I'm not picking up specifically on Java here - I think many languages neglect exceptions or error handling. Go is also an instructive example of a language that chose a non-exception-based error handling mechanism that the designers claimed to be superior, but failed to add ergonomics to that. This is not for the lack of trying though: the Go team tried to fix this issue multiple times, but there are very vocal parts of the Go community who opposed any kind of ergonomics, in favor of "explicitness" (as if explicitness means "error-prone boilerplate"). I would give the Go team full score for seriously trying.
I give them less score on the composition-over-inheritance part though. Go is one of the languages that has objects (structs) and interfaces, but disallows inheritance, but it doesn't provide any mechanism for automating delegation. Kotlin has shown that this is possible and even quite simple. It's not one of these languages features (like method overloading, type classes and generics) that carries a lot of corner cases and complexity that you have to deal with.
Checked exceptions do nothing to protect against those kinds of mistakes, which in my personal experience are vastly more common than whatever mistakes for which they may provide some protection
But that's a major flaw with checked exceptions – very often, whether an error is recoverable or not depends, not on the API itself, rather on how it is used. Yet checked exceptions force the API designer to make that decision while designing the API, when they can only guess at how it will be used.
A good example of this is FileNotFoundException – whether that is a recoverable error which ought to be handled, or whether there is nothing better to do than crash, depends on what the file is. If we are implementing a File Open dialog box in a GUI app – okay, we better catch the FileNotFoundException and display an error box, not just crash. But, suppose I am writing a micro-service, and the first thing it does on startup is read its config file, and the config file isn't there: is there any point in trying to handle that exception, or should it just crash? Obviously the designers of Java's file IO classes had the first scenario in mind more than the second, but it is an inherent flaw of checked exceptions that they forced them to make this decision at all.
From operations’ point of view the bug will be pinned on the owner of the service that is returning the JSON to the client/app and it’s up to them to trace it down the layers. A hierarchy of wrapped exceptions helps with that. Kind of like saying “I can’t do it because XYZ didn’t do what they were supposed to do”. So low key corporate blame game in code.
I know that the usual HN crowd doesn’t work at/know about companies that follow this, but this pattern is way more prevalent in the broader “IT” industry esp among offshore centres.
you basically end up needing to write your api twice.
On the other hand, a Result type makes errors part of the type system, and all other code can work with them "by default".
Unchecked exceptions, however, are a different beast entirely. They're not just checked exceptions without the checking; they fundamentally change how you code and how you think about error handling.
This means it is impossible to add new exceptions to library methods without breakage, and even in your own code it may mean hundreds of changes throughout your code to add a new exception type.
Rusts solution seems to have improved this conundrum, where you can describe how to map the new error into an existing hierarchy in a single or a few places instead of at every call site
That’s a feature, not a bug. If it wouldn’t, an exception of that kind could bubble up at a place you didn’t expect.
Also, it doesn’t require that many changes, it only has to be changed up to the point where you intend handling it.
I don’t see how rust would be immune to that.
Though it is true that polymorphism with respect to checked exceptions would be great. The new generation of languages might have that (e.g. Koka)
You should always be able to handle an unknown exception. Sure you can't do much about it, but it shouldn't be a big deal. An exception only occurs if the code can't continue as expected.
Libraries are supposed to be a point of abstraction. They should be allowed to change their implementation fundamentally as long as they continue to respect the same interface. Exceptions should not be part of the interface explicitly because are they implementation-detail related.
That's a big problem with Java's checked exceptions, it's not very composable and flow control can just stack up into deeper and deeper "waterfalls" of code rather than simpler pattern matching. (To be fair that's a big problem with exceptions in general as "flow control", they aren't very composable.)
Something like an Either monad in a language with sum types can be a great way to describe errors/exceptions usefully as return types. From a type theory perspective, checked exceptions can be seen as a hack for a language that doesn't support sum types and doesn't have great native monad binding.
Exceptions can be thrown from anywhere. That's the documentation.
head []
No monad, nothing, just an exception.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.
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.
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 disagree with their point though, in java the lib would've probably just converted the exception to a runtime exception so the API doesn't change...
The vast majority of cases, direct callers can't properly the handle the new error anyway. Callers either have to eat that exception, convert it (thus defeating the purpose of checked exceptions), or change their signature. Good programmers will change the signature and now every one of the callers of those methods have do the same thing. Ad infinitum.
But really the problem is actually far worse than that. Checked exceptions are brittle under any kind of implementation change. If you have a function that calculates a rate just using arithmetic. But tomorrow it loads a flat file. And next week it uses SQL. And in a month it calls a web service. As a consumer, this is none of your concern -- that's the whole point of abstraction. It's even worse when dynamic (polymorhphism) or functional (external code calls you).
What's the recovery from a SQLException anyway? How does that give you enough detail to do anything? You say SQLIntegrityConstraintViolationException but that's not the same type. Why does it matter that you declared it a SQLException over just Exception in that case?
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).
They’re literally the only way to report ctor errors.
> What Zig and Rust have as error handling mechanisms C++ has them too.
Only in the most reductive way that all langages are turing complete, in which case Java has them as well.
The point being? Please go find me a codebase which wraps object instantiation in try-catch clause if that's what you are trying to say. I haven't seen any.
Anyway, for that reason constructors are always written so that they cannot fail in hideous ways, and if there is no other way around it there is always 2-phase initialization (personally I think it's an anti-pattern).
> Only in the most reductive way that all langages are turing complete, in which case Java has them as well.
Nonsense. Zig AFAIU abstracts error types as some sort of integer enums while OTOH Rust has Result<T, E>. C++ has both of those mechanisms baked either into the language (enum) or standard library (std::expected, std::optional).
Why? In the simplest (but pretty common) case it's:
- successful response
- generic error message
> then pass it on and decide what to do with it at the system boundary
Yes, but if in 99% of cases I only pass it on, it's just visual noise. Noise you become blind to, and it loses meaning.
> Developers can tell from a glance at the resource method what the endpoint will return for each outcome, in context.
They can't really because you end up with some very generic error type anyway. Any typical service can have IOError, DBConnectionError, SQLError, OutOfMemoryError, StackOverflowError plus many other application specific errors. You end up with bulk of your methods returning Either<Error, Something> which is then meaningless.
Your controller method cannot act differently on a DBConnectionerror or OutOfMemory error.
Not to mention that exceptions cause developers to use them as control flow mechanisms.
For example searching a user by id. If the database returns 0 records, is that reason to throw an exception? Well, doing result[0] results in IndexOutOfBounds due to result being [].
But the reality is that the user not being there isn't exceptional. Typos are common. By using Result<T, E> or Either you enforce the developers to think more about their flow. One can write the method like this:
fn find_by_id(id: usize) ->Result<UserResult, Error> -> {
let raw_db_result = db.search("SELECT id, first_name, last_name FROM user WHERE id = ?", id)?;
match raw_db_result {
None => Ok(UserResult::NotFound),
Some(r) => {
let only_user = r.ensureOneResult()?;
let user = mapDBUserToUser(only_user);
Ok(UserResult::User(user))
}
}
}
What about Error? Reality is that I don't really care. Error doesn't contain anything that is actionable. Either the whole chain succeeds and returns a valid result or the Error. The caller wants to find a user by id. There is one, or there isn't. All the rest is just errors that they'll pass on too. And in the end they get logged and result into Error 500.A 404 is actually a valid result.
Now, if I were to use a throw new UserNotFoundException() for no user found you end up with generic try catches catching too much. And now someone needs to go and take it all apart to identify that single Exception that they want to deal with separately.
Whereas if I want to add a state in my enum the callers _MUST_ update their code due to how Rust works.
I'm not defining the errors like DBConnectionError or OutOfMemoryError - it's the framework/platform which defines them and throws/returns them.
> But the reality is that the user not being there isn't exceptional.
That depends. In some contexts it is not exceptional (getting user by ID given as an argument to webservice), in that case using Maybe type is great. In other contexts it is very much exceptional (e.g. signed JWT refers to a non-existing user) and throwing Exception makes more sense.
> What about Error? Reality is that I don't really care. Error doesn't contain anything that is actionable. Either the whole chain succeeds and returns a valid result or the Error. The caller wants to find a user by id. There is one, or there isn't. All the rest is just errors that they'll pass on too. And in the end they get logged and result into Error 500.
Which is the same as exception. But now you have this visual noise of something you claim you don't care about. An unchecked exception makes this irrelevant noise go away.
> Now, if I were to use a throw new UserNotFoundException() for no user found you end up with generic try catches catching too much. And now someone needs to go and take it all apart to identify that single Exception that they want to deal with separately.
try {
...
}
catch (UserNotFoundException e) {
// handle ...
}
I'm catching exactly the exception I want. Where I'm catching too much? Where do I need to take it apart?(This particular example of exception usage is bad, though, as it smells of exception control flow. Here using Maybe type would be better)
> Whereas if I want to add a state in my enum the callers _MUST_ update their code due to how Rust works.
Which is good for some cases where the enum describes "business" cases.
But it is pretty bad for truly exceptional cases (which are unlikely to be handled anyway). Library adding a new exceptional case will break its clients, which seems like a bad trade-off (again, for truly exceptional cases).
fun getUser(userId: UUID): User?
You cannot treat this result value as a `User` in code that calls this; though, once you null check it in your service layer, you can pass it on as `User` in the not-null branch. Null is nothing to be afraid of if the language forces you to declare nullability and deal with it.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
It is literally the compiler enforced shitty error handling from C’s errno that is not even a sum type.
If it wasn't for the error handling, I would probably use Go for some of my projects. It compiles to a native, self-contained binary and still has the convenience of a garbage collector, and of course has plenty of libs available due to popularity.
try {
...
}
catch (UserNotFoundException e) {
// handle ...
}
```This is just bikeshedding, because the equivalent code in Rust would be. For example, if there's multiple lines in the try block, who exactly returned this error? Are there other errors I didn't handle? Are there unexpected error that I forgot to check. For example, the "get" function of arrays in many languages usually always return T. But this is actually a lie because the array might be empty. So by right, it should return Option<T>. But exception based programming have basically created this illusion that its infallible. How many people check their array accesses?
```
match value {
Ok(ok)=> {...}
Err(UserNotFoundException(e)) = { handle }
e => return e
}
```
Which does look more complicated, but it scales way way better when you have multiple errors
> But it is pretty bad for truly exceptional cases (which are unlikely to be handled anyway).
But why should a language be designed for exceptional cases? Errors are not exceptional at all. In the above code, the actual code will actually look like this
```
let rows = db.get_rows(query)?; // returns Result<Vec<DbRow>, E1>
let first_row = rows.first()?; // returns Option<DbRow>
let user = first_row.to_user()?; // returns Result<User, E2>
return user
```
Exception-based language will have the same looking code, but then imagine what would happen if you try to figure out which functions return what. You have no other recourse other than to dig into the source code to find all the unchecked exceptions that it can throw.
Another example, how would an exception language write this code to get multiple rows from the db and map each row to its respective user.
```
// get_rows and to_user both can fail
let users :Vec<User> = db.get_rows(query)?.map(|r|r.to_user()).collect::<Result<_,_>()?;
```
on the other hand, depending on what your trying to do you might want to provide more context about what happened to the user/programmer
in swift you can change a throwing function to a nullable with `try?` so even if `getUser()` throws, you can keep it simple if thats what is appropriate
guard let user = try? getUser(id: someUUID) else {
return "user not found"
}
as an aside, swift "throws" exceptions but these are just sugar for returning basically an Result<T,E> and compose much better than traditional stack unwinding exceptions imoYou cannot catch a StackOverflowException or an OutOfMemoryException. All you do is log it & restart the app.
Once could split it into retryable errors and non-retryable ones. Like a DB disconnect, that can be retried.
BUT to be fair, that's not logic that belongs in the business.
It works great. Also, note that f() doesn't care about the type of error returned by g(), and that it will be a compilation error if g()'s error type turns into something incompatible.
Sadly, there are proposals to add exceptions to rust, and it seems likely they will be accepted, breaking error handling semantics across the entire ecosystem (even in new code, since exceptions are too hard to use correctly).
You can – by making f accept g's return types, including the error. This is even being done in the Go standard library: https://pkg.go.dev/text/template#Must
In a better langage f would take a Result, and then it can manipulate that however it wants.
Obviously you can also plug in adapters if you need some other composition e.g. g().map(f), g().and_then(f), …
Here's the thing. You've just described the vast majority of programmers - bad. They're really fucking bad. And language constructs that lead to bad programmers doing stupid things are, unfortunately making things worse for everyone.
Maybe a new testing paradigm will fix it, I certainly keep an eye out for them, but nothing so far. Property based testing is better, but mostly it just reminds me that we had Design By Contract 30 years ago.
I disagree -- this is the correct thing to do if you believe it is not possible for the checked exception to occur. (Catching it is wrong -- what would you do to correct something which you believe not to be possible? Forcing the caller to handle it is wrong -- if you don't know what to do with it, they sure won't!) Wrapping checked as unchecked encodes your belief that should it occur, it is a logic error, akin to out-of-bounds array access or null pointer dereference.
(Of course, swallowing expected exceptions one is simply too lazy to do anything about is poor practice! Not disagreeing with that.)
Actually no. For instance, the caller at some point up the stack may know if something is worth retrying.
If it is not possible to occur, then it should not be part of the API.
The only time I rethrow a checked exception as an unchecked exception is when the code is still under construction. The default of the eclipse code generator is to log and ignore caught transaction. I think wrapping into an unchecked one is the better default behavior for incomplete code under a "fail fast" policy.
Ah, but what if it can occur, just never with what you pass in? Suppose a function is documented to throw some checked exception if some parameter is a negative number, but you pass in a positive literal/constant? In such a situation, the checked exception will never occur! With Rust, for example, this is easily done with an `unwrap()` (and, possibly, a comment) to assert said belief, but with checked exceptions, there's no way to force the compiler to squash the required check.
The algebraic data type equivalent of this shows up all the time in functional code -- unwrapping a result type you know can't be Error/None/etc. because of logic. You don't rewrap it and force the caller to deal with a case which logically cannot occur; instead you throw an unchecked exception.
I don’t think that’s close to sufficient to making checked exceptions work tho, let alone good.
Unfortunately, this ends up propagating out and your entire code base needs to also do this. Tho i reckon it will make your code better in the future, for a short term/temporary pain converting/adding breaking changes etc.
That's not a given. It's perfectly fine to handle a checked exception via some custom runtime. The nice part is your your code base was given a chance to handle the key exceptions of this particular api all from your ide without you having to refer to documentation etc.
Moreover, it's good that code nearest the site where the exception is thrown handles the error, as only it has context for what's going on at the time. Code further up the stack won't have any clue what this random IOException might relate to.
If you're confident the IOExceptions can't occur under normal conditions -- say, you know the file has correct permissions, isn't being concurrently modified, etc. -- then, encode this belief by catching the IOException near to its origin and rethrowing as an unchecked exception.
This same pattern shows up even in languages without exceptions. In C -- always check errno; don't try to catch SIGSEGV or SIGABRT; raise SIGABRT if errno is something you don't plan to handle. In F# -- you're forced to match Ok/Error; don't try to catch exceptions; raise an exception if you don't expect Error.
Is it worth it to thread the IOException through the network layer, then the HTTP-client library, then then business serialisation up to the actual context?
I don't want a dozen checked exceptions. (and with this approach socket-limitations and connection loss should get a different exception.) I also don't want catch and rethrow. Unchecked+checked+docs+crashes are a measured approach.
Sometimes, your logic is flawed. A condition occurs which you erroneously deduced not to be possible.
Unchecked exceptions are the necessary manifestation of these unforeseen errors. Catching them is pointless, what will you do with them? Dynamically fix the logic of your program? What can be the reasonable response to "index out of bounds", try a different index? [1]
Depending on the domain it may be appropriate to convert them indiscriminately to checked exceptions at module boundaries (e.g. requests within a server) -- but within said module it remains pointless to catch them. (This is a form of swallowing.)
In other domains, crashing is the correct behavior. The code cannot proceed correctly, it must abort.
Checked exceptions are appropriate only when the exception can be anticipated and thus planned for, ideally by code closest to where it is thrown.
[1] Actually there was a paper a long time ago that monkey-patching C code to return fake "null" data in response to out-of-bounds memory accesses actually resulted in the intended (i.e. correct) behavior in the majority of cases. But I digress.
AFAIK, the only proposal related to exceptions is adding a `try` block that would scope the `?` operator to that block instead of the current function.
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.
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}!")
}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.
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
When recovery should not be attempted (example: "index out of bounds") then you don't want to declare or catch the exception, and that's when you use a subclass of RuntimeException in Java.
any real world library or application will catch Exception e, catch checked exceptions and turn them into unchecked ones etc etc
You might do it because you want to factor the handling of the different result cases out to another function.
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