Checked exceptions are unpopular since no one likes responsibility. But they are great when used right. Calls that must have proper cleanup after them e.g. SQL, IO are checked. The fact that this must be communicated via interfaces is hugely important.
There are "weird" problems such as stream close() throwing a checked exception. That's an API misbehavior that has a workaround thanks to try-with-resources.
This comes from a misunderstanding of the reason why exceptions where designed they way they were. The whole point of exceptions bubbling up w/o having to write support code to deal with passing exceptions further is to make it so that the purpose of the function is clear to the reader.
Go's exceptions have the same unfortunate property as Java's checked exception. And that's what makes Go's code atrocious. Every other line you see something like:
if x, e := f(); e != nil {
...
} else {
return y, e
}
It makes it very easy to make mistakes when you have to write a lot of repetitive code. You make typos, and because they often land on the "bad" path, they aren't immediately discovered. You have to memorize the state of your function wrt' variable initialization, because now you cannot automatically initialize and destroy them all together. You need to create a lot of helper variables whose purpose is only to transfer return value from one function to another...
If you think that you want checked exceptions, then you don't want exceptions at all. You are denying them the very purpose they were created for. But there are alternative ways to deal with unexpected events in program execution. Monads would be one of those. So... maybe just don't use exceptions?
No. This comes from a misunderstanding of checked exceptions.
Checked exceptions don't mean I need to handle the exception right now. They mean I need to either do that or declare throws. Declaring throws is fine it implicitly documents the code and enforces a similar requirement up the chain.
Checked exceptions aren't the default and shouldn't be.
> Go's exceptions have the same unfortunate property as Java's checked exception.
Wait what? Here's where you lost me. You don't "throw" errors in Golang. Instead it's common practice for functions to return multiple values, one of which can be an error. Errors are just structs that implement the Error interface.
The language (and the compiler by extension) doesn't make you explicitly handle errors. On the contrary: you can choose to ignore errors altogether.
On the other hand, a checked exception in Java is one that MUST be either caught or declared in the method in which it is thrown. Code that fails to do this won't compile. You're completely wrong here.
It's weird to argue that null is how the hardware works. Many structs contain only bytes that are allowed to take on any value, so making them nullable takes extra space.
We have it in Java too. For us it's try-with-resources and has nothing to do with checked exceptions. Checked exceptions remind you that you need to wrap the code and need to take that into account.
They also force you to declare that an exception is thrown if you don't want to handle it in the current method. That's important as the signature of the method carries an important failure that can't be dismissed and is enforced by the compiler.
Not exceptions, but zig has explicit error return types, and it works great. Especially since you can also just "tell the compiler to figure it out". There are a few cases where the compiler can't figure it out, but in those cases you grudgingly annotate the error types and it's done.
Yes and: The JIT will optimize away Null Object method invocations.
All my composable classes have a Null Object. No null checks means concise iteration (eg graph traversal). Eliminates NPEs. Just as fast.
Win / win / win.
Optionals (classes and operators) continue to be turrible mistakes. So much unnecessary effort, so little benefit.
Ditto @Nullable and @Nonnull.
Someday, maybe, I'll make a javac compiler plugin which autogenerates Null Object implementations and converts "Node abc = null" to "Node abc = Node.DEFAULT_NULL_OBJECT_INSTANCE".
It seems like when one is implementing Null Objects and making incomplete attempts to prevent values from ever being null it is not really an argument in favor of nullable values being good and the only type of values that should be present in a language.
Checked exceptions failed because 99 times out of 100 the exception is not recoverable, so the try/catch block is just wasting everyone's time. (In 7 years as a Java dev I can think of one time I wrote code that tried to recover from IOException instead of just making the caller retry.)
Even when the exceptions is theoretically recoverable, it has to get propagated up properly to the caller who should be handling recovery from it. But checked exceptions don't propagate sanely through executors and across RPC calls, so good luck with that.
The Task type in C# with the await unwrap sugar is similar to this. You can even check for and grab the exception without throwing if you want to.
Still, it's too bad the error type must be a throwable. I kind of wish it could just be a plain type so you can error or cancel without generating stack traces. Awaiting a failed task could still throw.
Would be a nice perf boost. As it is now, you don't want to actually cancel or fail a C# task in performance critical code. You need to successfully complete the Task and return an error, which is pretty confusing.
"throws IOException" is too much code? Or is the issue more that you can't really do autocoercion to a declared thrown type in Java the same way that you can do in Rust?
Proliferation of types is an issue in Java, but the whole language has that problem. It's not just exceptions.
Though I mostly agree it's slightly more subtle IMO. They failed because the writer of the method cannot know what the caller can recover from but must decide before compile time. The caller gets little say.
I don't think they are meant to be recoverable from, but instead they are a way to provide a controlled shut down of an irrecoverable failure, or to limit the blast radius of a localized failure.
In that sense, they are quite useful. Saving the file generated an exception - instead of suddenly closing the application, display an error. Or maybe a database is not accessible anymore. Instead of suddenly ending the service, trigger a controlled shutdown (log, send alerts, etc).
I don't think anyone would expect exceptions to propagate through RPC calls. A call fails, probably containing a description of the fail. Why should it propagate?
There are ways to handle errors and return errored RPCs without exceptions, such as internal error codes. But error codes are just as irrecoverable as exceptions.
In a desktop application that is not allowed to totally crash, or at least has to crash kind of gracefully, checked exceptions are useful.
But in the world that most Java devs live in, which is various flavors of RPC server, failing requests is fine. If lots of requests fail, your monitoring infra should page someone, and that someone will go log spelunking and figure out what's broken.
Very occasionally it turns out that the thing causing the RPC failures is a recoverable exception, and then you should wrap the problematic stuff in a try/catch block. (Often you'll wind up having to detect the recoverable error case by conditioning on substrings in the exception message, which the library owners will arbitrarily change in future releases. So make sure to write regression tests so you'll catch this when you upgrade the library. Java is fun!) But 99% of the time the failure is "network is busted" or "config was invalid" or "hard disk failed" and you should not be defensively programming against all those possibilities.
I always feel like the error domain changes as you go across program domains.
I keep returning to something about error handling that bothers me. People argue what the proper way to handle errors is without really considering that it's highly context sensitive. Which makes me think you should be able to pass an error handler down to lower level functions that tells them what to do when something bad happens. Sort of like recent ideas where you pass functions a allocator instead of them calling malloc() directly or whatever.
Is this a problem with checked exceptions or a problem with Java? Couldn't exceptions be treated as robustly as param and return types in the generic system so they are more composable? Why not a Future<V, E1> interface?
The argument that some exceptions will always be runtime (like oom) so checked exceptions are flawed is harder to argue against but I would also say it's a matter of opinion whether you feel like it's worth dropping entirely.
You can return union types in Java if you want that, although it'd be nice to have better first class support for them.
The issue though is that forcing people to handle irrecoverable exceptions is just kind of dumb. If I do file IO, I know it'll barf sometimes. Sometimes I care, but the vast majority of the time I don't.
A lot of Java servers have RPC endpoints that basically say "write X to file Y" or "read X from file Y" and if the S3 connection is busted, there's nothing the server can do about that. Someone has to go log spelunking, figure out what's wrong (network is misconfigured, AWS is having an outage, whatever) and fix it. So it is bad API design on Java's part to make every single one of those RPC endpoints wrap every single thing that does file IO in a try/catch.
Checked exceptions are used to make up for Java's inability to return more than one value, plus it's inability to wrap two values without defining a new type. In other words, I think checked exceptions are basically a symptom of a lack of object literal syntax. This is in addition to their status as a "cool language feature" that is a siren song to new, bright programmers looking to spice up their designs. Exceptions are in general problematic (they move the program counter in a disjoint way - "non-local change of control" I think it's called) so it makes this particular siren song doubly deadly.
It would probably be handy if someone wrote a pamphlet on "Refactoring Exceptions" to give teams the confidence to refactor exceptions out of their code. I'll offer $20 to Martin Fowler to write such a thing.
Exceptions “don’t move the program counter in disjoint ways”, they are part of the “structured gotos”. In fact, it has the same control flow as an early return does, with the handler being locally found in a parent’s (recursively) method body.
Also, the point about multiple return types is pointless — it already has Optional, a proper Return type is completely feasible to implement and use in Java. So is a Pair<A,B> if tuples are what you mean. Some are just not part of the standard lib by default.
But the issue about the multiple return types is not pointless at all: Optional types, and pair types, were added to the language way later than checked exceptions. We didn't even have actual generics back then! So the checked exceptions really were a way to get around lacking alternative error management features.
If backwards compatibility wasn't a concern, checked exceptions would probably go away in a version or two, and we'd have some kind of monadic error type instead. But Java takes this seriously, and the standard library itself has methods with checked exceptions, so the timeline to go from checked exceptions to something else is very long.
Yet it is rarely done. Lots of boilerplate to replace what is easily done in other languages. If java provided a native tuple type, new patterns would appeat. As it is, too many lines for 1 off returns. Send off a serialzed json and deserialize it later. Easier than specific dtos.
Great article with a lot of pithy insights. Here's one of the core issues with checked exceptions. 2nd sentence to the last is the point.
> Anders Hejlsberg: Yeah, well, Einstein said that, "Do the simplest thing possible, but no simpler." The concern I have about checked exceptions is the handcuffs they put on programmers. 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. It is sort of these dictatorial API designers telling you how to do your exception handling. They should not be doing that.
The issue with all exceptions, and error handling in general, is that only very rarely is there actually recourse for an error. For instance, in an HTTP request handler, the vast majority of errors will end up as something like a 500. Checked exceptions are annoying because they make you explicitly handle an error when you likely already have a handler in place to handle all exceptions, checked or otherwise.
To me, you kind of hit both the pros and cons of checked exceptions. It makes the calling code have to deal with an important error case. This is actually super useful for very important error situations (for instance, let's say you're writing some type of file-processing class, and the class was unable to open the file for some reason, it might be a good situation to throw a checked exception as this can probably happen reasonably often).
To me, the difficulty with exceptions is when writing a method that needs to throw an exception is deciding whether it should throw one that is actually checked. In fact, read a few of years ago most exceptions should actually be unchecked, but at least then, this was still greatly up for debate.
... as I type this, am realizing having the option of unchecked and checked exceptions is nice in that they reduce the amount error code you have to write. Because by throwing unchecked exceptions, this is error handling that all the entire calling code-chain doesn't need to deal with. The obvious situations are system errors like out of memory errors, missing resources... but also those programmatic errors that are unexpected, like array out of bounds, ClassCastException.
Also, the calling chain then has the option to handle it if it's needed. For instance, if you're writing a low-level messaging queue that needs to report system errors (btw, this was a real situation that happened to me). Just my two cents.
While I broadly agree with this, the author doesn't appear to go far enough himself.
> Functional error handling, using Option and Result types, is rapidly becoming the standard operating procedure in essentially every language, because it relies on nothing but values and types. They are more of the same, and so they fit right into the existing language machinery.
I agree that Optional types are better than checked exceptions, but they are worse than union types. A function with the union return type String|Error (read: String or Error) is allowed to return another function with return type String. Similarly, a function which accepts the type String|Error as a parameter also accepts parameters of type String. With Option types this doesn't work. Option<String> and String are incompatible types, so code has to be rewritten if the types change.
Personally, I would go so far as to say that union types should replace exceptions in general, checked or unchecked, as well as any implicit nullabiliy, which can be replaced with the explicit union type Foo|Null.
(Although some special syntax for handling Foo|Exception or Foo|Null is probably a good idea, as "error" and "nothing" are pretty general categories.)
I often run into the opposite problem: There are union types, but I would really like sum types (i.e. tagged unions). The common case is a data structure where I cannot guarantee that the user will not want to use Null as a value in my data structure, but I also need to represent the absence of values myself. With a sum type, my absence would be None and the user’s absence would be Some(None). With union types, I need to somehow create an extra sentinel value myself.
Yeah Containers like Option<T> and Result<T> not having a proper subtyping relation to T is major flaw. I mean you are basically giving a stronger guarantee if you are returning T instead of T or an Error, yet you break every callsite.
I have been thinking about this in terms of a data oriented programming language like clojure, but having types/schemas. If you use Union Types for something like getting a value out of a map for a specific key, then if you knew the schema of a specific map you could reduce the return type from {T, Error} to just the type of the value T that you know is there.
Basically a sufficiently smart compiler with the necessary information could make you not have to deal with errors at all in certain cases.
With Result/Option/Maybe this would not be possible. It would always infect the entire callstack and you would always have to deal with it.
The problem with union types for exceptions is that it becomes impossible to return an error as the correct case, or at least there is no way to discriminate any more. Functions that deal with errors also need to be able to have exceptional situations, and in many cases there are completely generic functions as well that can error out for generic reasons. A basic example is the elementary case of having a list of errors for some reason and indexing out of bounds producing an error, what if one have a list of values, which are all derived from itself accessing lists, each of which contains “Ok:Val+Err:OutOfBounds”, accessing that list itself now becomes indeterminate.
The other issue is that the language needs to have support for union types rather than merely sum types. And there's a good reason almost no statically typed language has support for union types since it removes provability and it's not something that can in easily be statically decided, requiring dynamic type tags, but then again, that's what sum types effectively are as well.
Typed Racket is statically typed and has union types, but it's an extra layer on top of a dynamically typed language, so the type tags where there already, but it's not easy to erase type data at run time not to implement.
In your example of “String|Error”, it essentially requires that every String is now tagged at runtime with tag information about it in practice.
I have extensive experience in C# as well as Java. It is bizarre to suggest that checked exceptions have failed. It is unchecked exceptions that have failed. It is the biggest flaw of C#, in fact. Why?
As an example, I wrote some very good C# code, carefully tested it, made it work flawlessly, then suddenly it started crashing. What happened? Someone made a change in a function I was calling, and it started throwing a new exception. This would have caused a compile error in Java, not a crash.
The list of recoverable exceptions that can be thrown by a method should be part of the contract. It should be written down by the programmer, and enforced by the compiler. If not, then to avoid crashing you would have to catch the root Exception class, which everyone agrees is a bad idea.
This is very simplistic view of the problem. This completely glosses over modularity, ABI, performance optimizations... just to name a few.
How are you going to write generic functions that take functions as arguments and re-throw the errors thrown by these functions, if you use checked exceptions? Will you require that the acceptable functions only throw exceptions that you like? -- Then your generic function is close to being worthless...
If exceptions are encoded in function's interface, then they have to be in ABI, but then you must have non-trivial types available when marshalling data between two components, so, you cannot serialize the communication using some protocol with a fixed number of types (eg. JSON and friends), because now you need to account for the infinite variety of exception types.
Because of at least these two things, what I saw happen a lot of Java / C++ projects (god blessed me with very little C# exposure) was that as soon as a developer encountered a function with checked exceptions, a wrapper was written which changed the type into a runtime exception. This is so because exceptions are supposed to be handled separately, and often the author of the function has no idea how they need to be handled -- so they want to concentrate on the main goal of the function. Once functions grow into garlands of try-catch-catch-catch-...catch the focus is lost. It becomes very hard to understand why the function was written in the first place, because the error handling takes over every other concern.
> Will you require that the acceptable functions only throw exceptions that you like? -- Then your generic function is close to being worthless...
Constrained generic parameters are actually super useful.
> If exceptions are encoded in function's interface, then they have to be in ABI
They already are in Itanium
> you cannot serialize the communication using some protocol with a fixed number of types (eg. JSON and friends), because now you need to account for the infinite variety of exception types.
No, the only exception that arises is ser/deserialization error. You can also trivially represent an error in JSON using an object, which is exactly what JSON RPC protocols do. It's also never safe to throw across an FFI boundary and similarly nonsensical to throw across a serialization boundary, so I'm not sure why you'd care.
> How are you going to write generic functions that take functions as arguments and re-throw the errors thrown by these functions, if you use checked exceptions?
I've actually done this with an interface type parameter used in a throws clause in Java before, but I'm not sure how it interacts with module boundaries, it's kind of verbose, and it certainly didn't seem to be common practice. But it did work: the HOF-ish method that took a parameterized ThingFrobber<E> and used it to frob things unchecked would compile only if it also threw E, and the method passing the HOF a ThingFrobber (I think via lambda syntax) could catch the associated concrete checked exception type to sink it. IIRC I was using it to allow a checked early exit from a complex iteration, and it even propagated a slightly complex bound with multiple checked exception types more smoothly than I'd expected it to.
> write generic functions that take functions as arguments and re-throw the errors thrown by these functions
There is a philosophy that applies here: simple things should be simple, complex things should be possible. The scenario you're mentioning is not common enough that the language design should be centered around it.
> Someone made a change in a function I was calling, and it started throwing a new exception.
If it has nothing to do with your code, why not either let it bubble up the stack or try/finally it to do whatever clean-up you need to do before re-throwing it and letting it move on to a global logger or similar.
If it is something you care about you can always catch the specific exception type and handle it.
Maybe whoever worked on the other function wasn't even aware it might throw that particular exception.
Granted I am far more experienced in C# than Java, but the idea that with every code change you potentially have to review and declare every single type of exception seems like madness ("what happens if we pull the cord when it's here, what type will that throw?").
Imagine that the functionality implemented by your method is very important. This functionality should not be needlessly aborted. If so you want to be aware of errors you can recover from, correct? This is why checked exceptions are helpful. It gives you a guaranteed-by-compiler list of exceptions and you can decide which of those you should recover from.
I stopped using Java a long time ago, and so I assume the language has gotten better since then, but early on at least it felt like Java almost took pride in making the developer jump through extra hoops. Compared to many other languages, using Java just made me feel tired.
Checked exceptions - a feature that seems to be a cost to the developer 100% of the time while being a benefit far less than 1% of the time - is a quintessential example of Java's tendency to cause developer fatigue.
Some people say the same about strong typing. Like, why do I have to write down the type of every single parameter or variable? Java is making me jump through hoops!
The point is, if you don't need the rigor of a strongly typed compiled language, there are other languages you can use. Perhaps a bash script is all you need.
Java almost took pride in making the developer jump through extra hoops
That's exactly it. A holier-than-thou stance, reprised by most comments in this post.
Checked exceptions were a PITA.
Someone has recommended to use another language if you didn't like it. Unfortunately my employer at that time had entered a partnership with Sun that prevented that :)
Another ideological prohibition: no pointers. Well, how do you assign event handlers then? People were extremely confused with anonymous classes. I got the reason instantly.
> Checked exceptions - a feature that seems to be a cost to the developer 100% of the time
That's because exceptions are overused for not-truly-exceptional conditions. Languages that throw exceptions when a dictionary does not have a key you're looking for are doing it wrong.
> The list of recoverable exceptions that can be thrown by a method should be part of the contract.
If it’s really recoverable, then should it actually be an exception? How often do you see exceptions that are recoverable?
> If not, then to avoid crashing you would have to catch the root Exception class, which everyone agrees is a bad idea.
I disagree that it’s a bad idea, having a catch-all exception handler to avoid crashing the entire app is a good idea.
Java also has RuntimeException, and with it, a whole host of exceptions that may occur but are not declared. Should every function that uses the division operator be marked with `throws ArithmeticException` since it may end up being a division by zero? That would quickly become absurd.
If it’s really recoverable, then should it actually be an exception?
That's the problem with exceptions in general. They're a hammer that makes everything look like a nail. People start using them for all kinds of control flow situations because they're more convenient than having to deal with a lack of type system support for optional values, etc.
You get to the point where you have a parser that is expecting a digit and it encounters an alphabetic character so it throws an exception!
It may not be recoverable locally, but the caller may know how.
Consider Files.createFile. It throws if the target already exists. That method has no way of knowing what the recovery is, however, the caller might.
It could be that the caller wants to explode, but maybe its part of some kind of singleton launch where the recovery is to pass some piece of data off to whoever created the file originally.
Maybe this is a bad example since there’s not much interesting in the return of Files. But it’s exceptional in that it’s an abort rather than a success.
That said, checked exceptions are still trash because they don’t work across thread boundaries or with futures. ExecutionException is the catch all baked into the design because they needed something. The idea could have been good though because naming error handling visible is useful. Java just has the misfortune of designing early before the kinks had been worked out and now we get nice stuff like Rust has.
Catching the root exception is not a bad idea. If you had caught the new exception what would you have done with it?
Most of the time, it’s either catch the exception, log it in a central logging system keep moving and have a central alerting system or catch the exception log it and crash the program.
> Catching the root exception is not a bad idea. If you had caught the new exception what would you have done with it?
Something relevant to the error condition, probably?
There may be some cases where the total set of possible errors is too large to meaningfully handle every one of them specifically (for instance if you call a high level GPU initialization routine that can fail in a myriad of ways) but that's not true in all cases. Maybe the new error was in fact recoverable, or maybe it calls for some more specific diagnostics.
At any rate I completely agree with the parent that changing the set throwable exceptions should be considered a breaking API change and should be enforced by the compiler. If an API method wants to future-proof and be able to throw anything, it can declare just that and everybody will know to expect the unexpected.
That's why I vastly prefer Rust's Result<> system which does most of what exceptions do and with fairly similar ergonomics but with normal return values and all the type checking that it involves.
> Someone made a change in a function I was calling, and it started throwing a new exception. This would have caused a compile error in Java, not a crash.
I call BS here. I've worked in a few Java projects, and in every single one, the people changing the method in question would have thrown RuntimeException to stop the compile errors.
If RuntimeException was checked, you might have a point, but given that there's a class of unchecked exceptions, you're relying on discipline instead of the compiler anyways.
Each 'leaf' statement of language should have a definite and finite list of exceptions that it can throw.
Each non-leaf statement is a composition of leaf statements. Therefore every exception can be determined, and that's our checked exceptions would work; work at the exceptions and check they match the list.
Therefore even without checked exceptions the compiler could provide the exceptions you want and you can check them or not if you please, so get the best of both worlds. All without forcing people to give an explicit list of exceptions everywhere. Forcing checked exceptions is not going to be popular, and much of the time is counter-productive.
Also someone broke your Liskof Substitution Principle which may be your bigger problem.
The issue brought up by the parent is that the exception needed not be unexpected. If the compiler had generated an error because a new exception type could be thrown by the third party API, the developer could have decided that they either couldn't do anything with it and just add a catchall, or actually implement code specifically to handle this issue.
IMO the entire concept of "unexpected exception" is bogus in and of itself. That's like calling "exit(1)" in your code when something goes wrong instead of using proper error handling facilities.
Admittedly it's very convenient for small scripts where proper error handling might be overkill, but for any serious application it's a massive footgun IMO.
You run the risk of swallowing something interesting. This was always the problem with C# exceptions to my mind: there was just one mechanism for reporting errors, which covered everything from the most non-erroneous of events, that aren't even errors, such as file not found or invalid file name encoding or failed to write data to file or socket error during write, to stuff that absolutely should cause the program to go pop and die immediately, such as a null pointer or divide by zero.
Anyway, I sympathize with this thread's OP, as I've had exactly the same problem with C#. In the end, I came to the conclusion you kind of do often have to catch every exception, because you can't trust the functions you call, and the documented exception lists for framework stuff are not always accurate. Safest is to put each call in its own try...catch block, and do any property accesses outside the try...catch block so you find out about the NullReferenceExceptions.
This is annoyingly verbose though.
It also does assume your setters and getters don't throw anything.
I mostly liked C#, but the exceptions aspect is not good. Similarly, I've generally despised working with Go, but the way it deals with errors is not its worst feature! (You do have to pay attention to the linter output though! It's the same err variable every time, so the compiler's fastidious checks for variable use are always foiled...)
When you catch the root Exception class in C# you end up catching IndexOutOfRangeException as well. You should let the program crash instead because this happened due to a bug in your program. Continuing as if nothing happened is unsafe.
Another issue is that you are not allowing higher layers see the exception, even though they may have logic for recovering from the exception. To see the exception they have to now fix your code by removing the catch-all.
Checked exceptions are unpopular since no one likes responsibility. But they are great when used right. Calls that must have proper cleanup after them e.g. SQL, IO are checked. The fact that this must be communicated via interfaces is hugely important.
There are "weird" problems such as stream close() throwing a checked exception. That's an API misbehavior that has a workaround thanks to try-with-resources.
Go's exceptions have the same unfortunate property as Java's checked exception. And that's what makes Go's code atrocious. Every other line you see something like:
It makes it very easy to make mistakes when you have to write a lot of repetitive code. You make typos, and because they often land on the "bad" path, they aren't immediately discovered. You have to memorize the state of your function wrt' variable initialization, because now you cannot automatically initialize and destroy them all together. You need to create a lot of helper variables whose purpose is only to transfer return value from one function to another...If you think that you want checked exceptions, then you don't want exceptions at all. You are denying them the very purpose they were created for. But there are alternative ways to deal with unexpected events in program execution. Monads would be one of those. So... maybe just don't use exceptions?
Checked exceptions don't mean I need to handle the exception right now. They mean I need to either do that or declare throws. Declaring throws is fine it implicitly documents the code and enforces a similar requirement up the chain.
Checked exceptions aren't the default and shouldn't be.
Wait what? Here's where you lost me. You don't "throw" errors in Golang. Instead it's common practice for functions to return multiple values, one of which can be an error. Errors are just structs that implement the Error interface.
The language (and the compiler by extension) doesn't make you explicitly handle errors. On the contrary: you can choose to ignore errors altogether.
On the other hand, a checked exception in Java is one that MUST be either caught or declared in the method in which it is thrown. Code that fails to do this won't compile. You're completely wrong here.
Why? In C# you use a “using” block and whether your code exits the using block successfully or with an exception, the cleanup is automatic.
RAII has been around at least since the 80s with C++.
They also force you to declare that an exception is thrown if you don't want to handle it in the current method. That's important as the signature of the method carries an important failure that can't be dismissed and is enforced by the compiler.
> Null is fast. Super fast. Literally free.
Yes and: The JIT will optimize away Null Object method invocations.
All my composable classes have a Null Object. No null checks means concise iteration (eg graph traversal). Eliminates NPEs. Just as fast.
Win / win / win.
Optionals (classes and operators) continue to be turrible mistakes. So much unnecessary effort, so little benefit.
Ditto @Nullable and @Nonnull.
Someday, maybe, I'll make a javac compiler plugin which autogenerates Null Object implementations and converts "Node abc = null" to "Node abc = Node.DEFAULT_NULL_OBJECT_INSTANCE".
https://en.wikipedia.org/wiki/Null_object_pattern
Even when the exceptions is theoretically recoverable, it has to get propagated up properly to the caller who should be handling recovery from it. But checked exceptions don't propagate sanely through executors and across RPC calls, so good luck with that.
The try/catch construct require too much code for the common propagate case.
It would be nice if Java had a similar construct for error handling.
Still, it's too bad the error type must be a throwable. I kind of wish it could just be a plain type so you can error or cancel without generating stack traces. Awaiting a failed task could still throw.
Would be a nice perf boost. As it is now, you don't want to actually cancel or fail a C# task in performance critical code. You need to successfully complete the Task and return an error, which is pretty confusing.
Proliferation of types is an issue in Java, but the whole language has that problem. It's not just exceptions.
In that sense, they are quite useful. Saving the file generated an exception - instead of suddenly closing the application, display an error. Or maybe a database is not accessible anymore. Instead of suddenly ending the service, trigger a controlled shutdown (log, send alerts, etc).
I don't think anyone would expect exceptions to propagate through RPC calls. A call fails, probably containing a description of the fail. Why should it propagate?
There are ways to handle errors and return errored RPCs without exceptions, such as internal error codes. But error codes are just as irrecoverable as exceptions.
But in the world that most Java devs live in, which is various flavors of RPC server, failing requests is fine. If lots of requests fail, your monitoring infra should page someone, and that someone will go log spelunking and figure out what's broken.
Very occasionally it turns out that the thing causing the RPC failures is a recoverable exception, and then you should wrap the problematic stuff in a try/catch block. (Often you'll wind up having to detect the recoverable error case by conditioning on substrings in the exception message, which the library owners will arbitrarily change in future releases. So make sure to write regression tests so you'll catch this when you upgrade the library. Java is fun!) But 99% of the time the failure is "network is busted" or "config was invalid" or "hard disk failed" and you should not be defensively programming against all those possibilities.
I keep returning to something about error handling that bothers me. People argue what the proper way to handle errors is without really considering that it's highly context sensitive. Which makes me think you should be able to pass an error handler down to lower level functions that tells them what to do when something bad happens. Sort of like recent ideas where you pass functions a allocator instead of them calling malloc() directly or whatever.
The argument that some exceptions will always be runtime (like oom) so checked exceptions are flawed is harder to argue against but I would also say it's a matter of opinion whether you feel like it's worth dropping entirely.
The issue though is that forcing people to handle irrecoverable exceptions is just kind of dumb. If I do file IO, I know it'll barf sometimes. Sometimes I care, but the vast majority of the time I don't.
A lot of Java servers have RPC endpoints that basically say "write X to file Y" or "read X from file Y" and if the S3 connection is busted, there's nothing the server can do about that. Someone has to go log spelunking, figure out what's wrong (network is misconfigured, AWS is having an outage, whatever) and fix it. So it is bad API design on Java's part to make every single one of those RPC endpoints wrap every single thing that does file IO in a try/catch.
It would probably be handy if someone wrote a pamphlet on "Refactoring Exceptions" to give teams the confidence to refactor exceptions out of their code. I'll offer $20 to Martin Fowler to write such a thing.
Also, the point about multiple return types is pointless — it already has Optional, a proper Return type is completely feasible to implement and use in Java. So is a Pair<A,B> if tuples are what you mean. Some are just not part of the standard lib by default.
If backwards compatibility wasn't a concern, checked exceptions would probably go away in a version or two, and we'd have some kind of monadic error type instead. But Java takes this seriously, and the standard library itself has methods with checked exceptions, so the timeline to go from checked exceptions to something else is very long.
Yet it is rarely done. Lots of boilerplate to replace what is easily done in other languages. If java provided a native tuple type, new patterns would appeat. As it is, too many lines for 1 off returns. Send off a serialzed json and deserialize it later. Easier than specific dtos.
Yeah, that doesn't sound disjoint /s
Relevant JEPs:
- 441: Pattern Matching for switch
- 440: Record Patterns
- 409: Sealed Classes
- 395: Records
It is kind of similar to Scalas version of pattern matching with case classes.
https://www.artima.com/articles/the-trouble-with-checked-exc...
> Anders Hejlsberg: Yeah, well, Einstein said that, "Do the simplest thing possible, but no simpler." The concern I have about checked exceptions is the handcuffs they put on programmers. 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. It is sort of these dictatorial API designers telling you how to do your exception handling. They should not be doing that.
To me, the difficulty with exceptions is when writing a method that needs to throw an exception is deciding whether it should throw one that is actually checked. In fact, read a few of years ago most exceptions should actually be unchecked, but at least then, this was still greatly up for debate.
... as I type this, am realizing having the option of unchecked and checked exceptions is nice in that they reduce the amount error code you have to write. Because by throwing unchecked exceptions, this is error handling that all the entire calling code-chain doesn't need to deal with. The obvious situations are system errors like out of memory errors, missing resources... but also those programmatic errors that are unexpected, like array out of bounds, ClassCastException.
Also, the calling chain then has the option to handle it if it's needed. For instance, if you're writing a low-level messaging queue that needs to report system errors (btw, this was a real situation that happened to me). Just my two cents.
> Functional error handling, using Option and Result types, is rapidly becoming the standard operating procedure in essentially every language, because it relies on nothing but values and types. They are more of the same, and so they fit right into the existing language machinery.
I agree that Optional types are better than checked exceptions, but they are worse than union types. A function with the union return type String|Error (read: String or Error) is allowed to return another function with return type String. Similarly, a function which accepts the type String|Error as a parameter also accepts parameters of type String. With Option types this doesn't work. Option<String> and String are incompatible types, so code has to be rewritten if the types change.
Personally, I would go so far as to say that union types should replace exceptions in general, checked or unchecked, as well as any implicit nullabiliy, which can be replaced with the explicit union type Foo|Null.
(Although some special syntax for handling Foo|Exception or Foo|Null is probably a good idea, as "error" and "nothing" are pretty general categories.)
I have been thinking about this in terms of a data oriented programming language like clojure, but having types/schemas. If you use Union Types for something like getting a value out of a map for a specific key, then if you knew the schema of a specific map you could reduce the return type from {T, Error} to just the type of the value T that you know is there.
Basically a sufficiently smart compiler with the necessary information could make you not have to deal with errors at all in certain cases. With Result/Option/Maybe this would not be possible. It would always infect the entire callstack and you would always have to deal with it.
The other issue is that the language needs to have support for union types rather than merely sum types. And there's a good reason almost no statically typed language has support for union types since it removes provability and it's not something that can in easily be statically decided, requiring dynamic type tags, but then again, that's what sum types effectively are as well.
Typed Racket is statically typed and has union types, but it's an extra layer on top of a dynamically typed language, so the type tags where there already, but it's not easy to erase type data at run time not to implement.
In your example of “String|Error”, it essentially requires that every String is now tagged at runtime with tag information about it in practice.
For what it's worth, I learned about union types from Ceylon, which was statically typed.
As an example, I wrote some very good C# code, carefully tested it, made it work flawlessly, then suddenly it started crashing. What happened? Someone made a change in a function I was calling, and it started throwing a new exception. This would have caused a compile error in Java, not a crash.
The list of recoverable exceptions that can be thrown by a method should be part of the contract. It should be written down by the programmer, and enforced by the compiler. If not, then to avoid crashing you would have to catch the root Exception class, which everyone agrees is a bad idea.
More on checked vs unchecked exceptions here: https://forum.dlang.org/thread/hxhjcchsulqejwxywfbn@forum.dl...
How are you going to write generic functions that take functions as arguments and re-throw the errors thrown by these functions, if you use checked exceptions? Will you require that the acceptable functions only throw exceptions that you like? -- Then your generic function is close to being worthless...
If exceptions are encoded in function's interface, then they have to be in ABI, but then you must have non-trivial types available when marshalling data between two components, so, you cannot serialize the communication using some protocol with a fixed number of types (eg. JSON and friends), because now you need to account for the infinite variety of exception types.
Because of at least these two things, what I saw happen a lot of Java / C++ projects (god blessed me with very little C# exposure) was that as soon as a developer encountered a function with checked exceptions, a wrapper was written which changed the type into a runtime exception. This is so because exceptions are supposed to be handled separately, and often the author of the function has no idea how they need to be handled -- so they want to concentrate on the main goal of the function. Once functions grow into garlands of try-catch-catch-catch-...catch the focus is lost. It becomes very hard to understand why the function was written in the first place, because the error handling takes over every other concern.
Constrained generic parameters are actually super useful.
> If exceptions are encoded in function's interface, then they have to be in ABI
They already are in Itanium
> you cannot serialize the communication using some protocol with a fixed number of types (eg. JSON and friends), because now you need to account for the infinite variety of exception types.
No, the only exception that arises is ser/deserialization error. You can also trivially represent an error in JSON using an object, which is exactly what JSON RPC protocols do. It's also never safe to throw across an FFI boundary and similarly nonsensical to throw across a serialization boundary, so I'm not sure why you'd care.
I've actually done this with an interface type parameter used in a throws clause in Java before, but I'm not sure how it interacts with module boundaries, it's kind of verbose, and it certainly didn't seem to be common practice. But it did work: the HOF-ish method that took a parameterized ThingFrobber<E> and used it to frob things unchecked would compile only if it also threw E, and the method passing the HOF a ThingFrobber (I think via lambda syntax) could catch the associated concrete checked exception type to sink it. IIRC I was using it to allow a checked early exit from a complex iteration, and it even propagated a slightly complex bound with multiple checked exception types more smoothly than I'd expected it to.
There is a philosophy that applies here: simple things should be simple, complex things should be possible. The scenario you're mentioning is not common enough that the language design should be centered around it.
If it has nothing to do with your code, why not either let it bubble up the stack or try/finally it to do whatever clean-up you need to do before re-throwing it and letting it move on to a global logger or similar.
If it is something you care about you can always catch the specific exception type and handle it.
Maybe whoever worked on the other function wasn't even aware it might throw that particular exception.
Granted I am far more experienced in C# than Java, but the idea that with every code change you potentially have to review and declare every single type of exception seems like madness ("what happens if we pull the cord when it's here, what type will that throw?").
Checked exceptions - a feature that seems to be a cost to the developer 100% of the time while being a benefit far less than 1% of the time - is a quintessential example of Java's tendency to cause developer fatigue.
The point is, if you don't need the rigor of a strongly typed compiled language, there are other languages you can use. Perhaps a bash script is all you need.
That's exactly it. A holier-than-thou stance, reprised by most comments in this post.
Checked exceptions were a PITA.
Someone has recommended to use another language if you didn't like it. Unfortunately my employer at that time had entered a partnership with Sun that prevented that :)
Another ideological prohibition: no pointers. Well, how do you assign event handlers then? People were extremely confused with anonymous classes. I got the reason instantly.
That's because exceptions are overused for not-truly-exceptional conditions. Languages that throw exceptions when a dictionary does not have a key you're looking for are doing it wrong.
If it’s really recoverable, then should it actually be an exception? How often do you see exceptions that are recoverable?
> If not, then to avoid crashing you would have to catch the root Exception class, which everyone agrees is a bad idea.
I disagree that it’s a bad idea, having a catch-all exception handler to avoid crashing the entire app is a good idea.
Java also has RuntimeException, and with it, a whole host of exceptions that may occur but are not declared. Should every function that uses the division operator be marked with `throws ArithmeticException` since it may end up being a division by zero? That would quickly become absurd.
That's the problem with exceptions in general. They're a hammer that makes everything look like a nail. People start using them for all kinds of control flow situations because they're more convenient than having to deal with a lack of type system support for optional values, etc.
You get to the point where you have a parser that is expecting a digit and it encounters an alphabetic character so it throws an exception!
Consider Files.createFile. It throws if the target already exists. That method has no way of knowing what the recovery is, however, the caller might.
It could be that the caller wants to explode, but maybe its part of some kind of singleton launch where the recovery is to pass some piece of data off to whoever created the file originally.
Maybe this is a bad example since there’s not much interesting in the return of Files. But it’s exceptional in that it’s an abort rather than a success.
That said, checked exceptions are still trash because they don’t work across thread boundaries or with futures. ExecutionException is the catch all baked into the design because they needed something. The idea could have been good though because naming error handling visible is useful. Java just has the misfortune of designing early before the kinks had been worked out and now we get nice stuff like Rust has.
E.g. a network call failing due to some IOException can be easily retried, that’s a proper error handling.
Most of the time, it’s either catch the exception, log it in a central logging system keep moving and have a central alerting system or catch the exception log it and crash the program.
Something relevant to the error condition, probably?
There may be some cases where the total set of possible errors is too large to meaningfully handle every one of them specifically (for instance if you call a high level GPU initialization routine that can fail in a myriad of ways) but that's not true in all cases. Maybe the new error was in fact recoverable, or maybe it calls for some more specific diagnostics.
At any rate I completely agree with the parent that changing the set throwable exceptions should be considered a breaking API change and should be enforced by the compiler. If an API method wants to future-proof and be able to throw anything, it can declare just that and everybody will know to expect the unexpected.
That's why I vastly prefer Rust's Result<> system which does most of what exceptions do and with fairly similar ergonomics but with normal return values and all the type checking that it involves.
See https://learn.microsoft.com/en-us/dotnet/fundamentals/code-a...
I call BS here. I've worked in a few Java projects, and in every single one, the people changing the method in question would have thrown RuntimeException to stop the compile errors.
If RuntimeException was checked, you might have a point, but given that there's a class of unchecked exceptions, you're relying on discipline instead of the compiler anyways.
Seriously, Java doesn't force bad programmers to write good code. It just enables good programmers to write good code.
We all do.. its always that other guy who is at fault :)
Each non-leaf statement is a composition of leaf statements. Therefore every exception can be determined, and that's our checked exceptions would work; work at the exceptions and check they match the list.
Therefore even without checked exceptions the compiler could provide the exceptions you want and you can check them or not if you please, so get the best of both worlds. All without forcing people to give an explicit list of exceptions everywhere. Forcing checked exceptions is not going to be popular, and much of the time is counter-productive.
Also someone broke your Liskof Substitution Principle which may be your bigger problem.
It's a bad idea to catch an unexpected exception?
IMO the entire concept of "unexpected exception" is bogus in and of itself. That's like calling "exit(1)" in your code when something goes wrong instead of using proper error handling facilities.
Admittedly it's very convenient for small scripts where proper error handling might be overkill, but for any serious application it's a massive footgun IMO.
Anyway, I sympathize with this thread's OP, as I've had exactly the same problem with C#. In the end, I came to the conclusion you kind of do often have to catch every exception, because you can't trust the functions you call, and the documented exception lists for framework stuff are not always accurate. Safest is to put each call in its own try...catch block, and do any property accesses outside the try...catch block so you find out about the NullReferenceExceptions.
This is annoyingly verbose though.
It also does assume your setters and getters don't throw anything.
I mostly liked C#, but the exceptions aspect is not good. Similarly, I've generally despised working with Go, but the way it deals with errors is not its worst feature! (You do have to pay attention to the linter output though! It's the same err variable every time, so the compiler's fastidious checks for variable use are always foiled...)
Another issue is that you are not allowing higher layers see the exception, even though they may have logic for recovering from the exception. To see the exception they have to now fix your code by removing the catch-all.
Oh dear. I have a slew of current counter examples on my machine, in GitHub, etc.
Either carelessly untrue or else annoying hypebole.
Made the rest very hard for me to read.