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.
Declaring throws end up leaking layers of abstractions in practice. Many thrown checked exceptions are not appropriate to pass above certain layers. It is often not OK to expose checked exceptions that are really implementation details up the chain—-it’s not a “similar requirement”—-it is exactly the same requirement and often an inappropriate one at many levels up the chain.
If you just pass exceptions up in throws, you end up causing cascading checked exceptions chain changes and creating a bunch of noise, which is what many times people resort to doing in practice, because they don’t have the experience and it is very rare/almost never done to have any kind of automatic enforcement/linters/static analysis that prohibits implementation detail checked exceptions from propagating past above certain levels.
It is almost never OK to propagate IOException, for example, past maybe 1 or 2 levels of private helper methods, almost never should be propagated past your class boundary and you have to think twice about adding throws to protected methods even, unless your subclass implementations are specifically implementing alternate IO calls.
So yes that’s why checked exceptions have failed. And when you actually take this extreme discipline to your code, yes you end up with a ton of try/catch handling noise and a whole lotta new exceptions classes, and it takes similar extreme discipline to always avoid poor/improper try/catch handlers. Refactoring code across certain boundaries becomes a much bigger hassle, so your end up discouraging refactoring across those boundaries, which leads to code that is more easily stale and design decisions that are much harder to back out of.
I think we're actually not that far off in our opinions.
Throwing an exception is an implicit part of our contract, whether we declare it or not. Since any method can throw an exception that implementation detail is already exposed we just don't know about it.
Yes. Propagating checked exceptions through a long chain is indeed a pita and as I said before, we need to be vigilant about using them correctly. But if I have library or infrastructure code that is doing IO/SQL, I still want people to know and handle the failure correctly.
I think they were never picked up in other languages because no one likes to tidy their room. You want to write a unit test or a hello world and suddenly there's a checked exception... No fun. Also it was used for many APIs that people felt were built badly (e.g. the infamous encoding exception, URL format, etc.).
> Throwing an exception is an implicit part of our contract, whether we declare it or not
I think this is the main reason why checked exceptions and errors as values work. The alternative is having to always specify all the exceptions in documentation so the caller knows what to expect, and hope that documentation covers everything and doesn't get out of date. That also has to propagate if the caller doesn't handle all exceptions, just like checked exceptions.
It's much better to encode this directly with checked exceptions or Result<T, E>. Why people like errors as values so much but dislike checked exceptions is beyond me. They're basically the same thing with different syntax.
You make a great point about exposing the exceptions of library code. Why _don't_ linters deal with this better? It seems like you could throw up a yellow squiggly on any public method that throws an exception outside of its package or some such thing.
I agree that it takes a lot of discipline to do this properly. Is that discipline alleviated with unchecked exceptions or would you say that doing things right takes the same work, checked or unchecked?
The fundamental thing an exception does is allow you to choose where in the stack to react to the exception. The fundamental thing a checked error does is force you to actually choose (even if that choice is to bubble all the way up and crash the program), rather than forget and end up with a bug.
The choice about where to handle the exception is almost always a good thing, since it reduces boilerplate in dealing with that condition. The cases where it's not a good thing are when it's either impossible to deal with the condition at all (e.g. assertion failure) or where it must obviously be handled locally (e.g. errno==EAGAIN type stuff).
The requirement to actually make the decision is a good thing in some cases (e.g. I/O), but not in other cases (e.g. out of memory) because the overhead of making this decision so often is larger than the benefit of avoiding that class of bug. It's beneficial when it's something that can only happen while doing certain well defined activities.
There are also places where there just shouldn't be an error at all. E.g. null pointers shouldn't be an error; your program should be rejected if the compiler can't prove that this is impossible through static analysis (even if that just forces the author to write assert(ptr!=0)).
I guess I'm converging on a scheme that looks like: I/O errors are checked exceptions. Out of memory is an unchecked exception. Assertion failures are panics which cannot be caught. Division by zero is prevented by static analysis. I'm not sure what should be done with EAGAIN/EWOULDBLOCK type stuff -- it doesn't really feel like those should be exceptions, but introducing a whole new "error" handling idiom just for this doesn't feel great either.
Yes, that's intentional. They are still exceptions because there are rare cases where you want to catch them. They're unchecked because it's not worth the hassle of declaring them everywhere. Python's KeyboardInterrupt is another great example -- it can theoretically happen almost everywhere, but most scripts don't have anything sensible to do with it and it will never happen in a daemon or GUI program. If you happen to be writing a REPL, though, it could be useful to just interrupt the last command.
I'm not sure interrupt is the right idiom there either. An actor or coroutine dedicated to processing keyboard input seems more sensible, precisely because interrupts can happen almost anywhere, and non-determinism is not something you want to introduce accidentally or implicitly.
Null pointer exceptions shouldn't exist in the first place, they're fairly trivial to avoid. Index out of bounds can be similarly avoided, although the cost in terms of economics is a lot steeper. I like Rust's strategy of providing a checked and an unchecked indexing mechanism, where the unchecked indexing mechanism typically crashes the program rather than just throwing an exception.
Illegal arguments can often be avoided in the first place with sensible types (although, like null pointer exceptions, this typically requires some language support), and unsupported encodings should be rare assuming widespread UTF-8.
So yes, I agree, those should all be very rare exceptions to find in a function signature.
Notice that parent comment was directed to a person who has the opinion of "everything should be a checked exception"... Not towards you.
Crashing the program isn't an option for a lot of applications. All those exceptions give me a runtime stack that I can log and an error that's very easy to fix quickly. The first 3 are runtime exceptions which means you won't know about them and don't need to write defensive code.
I love rust and its compile time checks. Checked exceptions are very similar to the concepts in rust, check as much as possible during compilation. However, this makes rust code non-trivial for some dynamic behaviors and sophisticated object graphs. There's a balance that we usually choose based on a languages target demographic. Rust is great for system programming, Java is great for enterprise programming. Both are very different domains with very different needs.
The discussion about null is a huge one and I'm already spending way too much time in HN comments. Forgive me if I don't open that damn can of worms ;-)
I think almost everything should be a checked exception, with some possible exceptions (haha) being things like out-of-memory exceptions (although even then, with a good type system, I can imagine them optionally throwing checked exceptions). So it's definitely written at me!
Rust is not the only language that handles these sorts of issues, and it's not the only way of doing that. OCaml also very neatly avoids most of these problems (again, indexing can optionally be checked), although it also includes exceptions on top of that. Even Typescript can be written in a style where exceptions, although interfacing with third-party code is a lot more difficult then. You can even configure the Typescript compiler to enforce checked indexing and prevent index-out-of-bounds errors.
The point is that there is nothing fundamental about these exceptions, other than that many languages have been designed with the assumption that they must be present and generally possible. But there's no reason to keep with that assumption, and we can write good code - and even good enterprise code, as demonstrated by Typescript - without those assumptions.
I find it disappointing that we expect so little from our tools that we tolerate these sorts of problems.
The first 3 need to terminate your thread because something is irretrievably incorrect. The only sane thing to do is stop.
"UnsupportedEncodingException" has lots of things you can do to recover. You can try different encodings. You can request retry of someting. You can check a CRC for corruption. etc.
So, the first 3 really shouldn't litter your code. The final one probably should.
For other readers the first 3 are all runtime exceptions and the last one is a checked exception that a lot of people complain about...
I think the last one is an example of why people hate checked exceptions and the solution to this was to create newer APIs that focus on UTF-8 that just don't throw that exception.
What about them? If your language semantics are pervaded by exceptions, that kinda suggests your language sucks. If you're unlucky enough to find yourself in this situation, then make exception contracts simple to express and propagate.
You have to think about exception polymorphism. If you don't have exception inference, you also have to think about manually propagating exception contracts. Declaring it once is not a big deal, doing it over and over again is super annoying.
Let's say I have a Collection and want to do IO within that collection... If IOException was a runtime exception I could just write that code without handling it and an unsuspecting user of my new IOCollection would suddenly get an IOException. That means I need to explicitly deal with it in my class and can't add a serious exception to the behavior of the class.
Yes, it can still throw a runtime exception which is why the separation of the two is so important.
OTOH we have InputStream and OutputStream. Both throw an IOException for all their methods. So if I have one of them I should always handle the exception which is always the right thing to do...
But you might say, wait... What if I have a theoretical InputStream that will never throw an IOException?
Don't fret, we have that. It's called a ByteArrayInputStream and works roughly like this:
ByteArrayInputStream bos = new ByteArrayInputStream(new byte[100]);
int value = bos.read();
Notice I didn't use try and catch. Why? Because neither the constructor nor the read method throw an IOException which is legal in Javas polymorphism implementation. However, the following code won't compile without a catch exactly because I need to handle IOException for the generic case:
InputStream bos = new ByteArrayInputStream(new byte[100]);
int value = bos.read();
What do you mean when you say "exceptions should be rare"? Do you mean they should not occur frequently during runtime, or that it should be rare to see explicit exception handling in a piece of code?
Parsing input, handling I/O is a good chunk of what a lot of run of the mill software does. Since this is Java and exceptions are the main way of expressing fallibility, I’m at a loss as to what should code that needs to indicate fallibility should be using other than exceptions.
Return values obviously, with suitably designed method contracts. Some (maybe most) types of I/O errors are proper exceptions. Parsing failures are not.
So now you have two different error paths, one in which the file can't be accessed, and one in which the file can be accessed but the contents are corrupted? The developer must handle exceptions and they must still check if the function returned some sentinel error value?
What's the point of forcing two different error paths on the developer?
They are different classes of errors, so you want different error paths. Parsing errors are common and should be handled locally with all of the usual logic. Exceptions are not supposed to be common errors which is why they trigger non-local control flow.
I'm curious how this is possible? Exception throwing and handling is fundamentally a flow control construct. ie: throwing an exception must control flow. A counter-example snippet where throwing an exception does not control flow would be appreciated.
He means exceptions are not for common control flow that shows up everywhere, like a return value. You want an exceptional control flow change when your hard drive runs out of storage, but you don't want exceptional control flow change when your hashtable doesn't have the key you're looking for.
The categories and frequency of these conditions are very different.
I don't think cases encountered in real work are really that unclear. It can sometimes seem unclear if you're used to language semantics that are inherently riddled with possible error states (like languages that allow null).
An exception would be if someone tried to get the parking status for "Blarghsday" from the function or some other impossible state (We all know time is weird[0])
If the expected operation is to return parking allowed status for a specified date or a weekday, I'd expect the function to return true or false unless the input is malformed (Actually I'd prefer it to return some kind of object that can give more context to the true/false, but I digress).
During normal operation it should never throw an exception. Because if I get an exception, I'll most likely either throw it up the stack or log an error, which will ping someone in PagerDuty if it happens too often.
What's normal operation? It sounds rather subjective.
It sounds like there are those who "expect the unexpected" and those who don't, and this plays out in weird programming language usage discussions. Among all the expectations that one could have about a program, people have the impossible choice of being either complete and inconsistent or incomplete and consistent in their operational definition of "normal". Is it not?
It says the word exception right there on the sign.
People keep parroting “exceptions should be for exceptions” like it means anything. Just because when it was first used it was given a particular name doesn’t really tell you anything about how it should be used.
> Checked exceptions don't mean I need to handle the exception right now.
Yes it does. You either use try-catch-catch-...-catch, or you append the new exception to the ever growing list of exceptions your function might throw. Even worse: when two functions called by the function you design can throw the same type of exception but for different reasons (eg. two functions cannot find a file, but those are different files), then, if you were a diligent and mindful programmer, you'd have to catch both and re-throw with new types, so that upstream could differentiate between the two failures, adding even more crutft into your code.
If it's an evergrowing list then you're using checked exceptions wrong and they highlight to you the problem of too many responsibilities in a single application.
Notice that IOException is one exception, you don't need to declare FileNotFoundException because it is an IOException. There's a similar hierarchy in an SQLException.
That's part of the beauty of checked exceptions. If your throws statement becomes too long or its stack carries too deep then you know you have a problem. This would be hard to notice otherwise.
> 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.
The point GP was making is that both Go and Java (in code which makes heavy use of checked exceptions and different exception types) mix together the business logic with error propagation. So, even though in Java you throw exceptions and in Go you return error values, they both end up (or can end up, in the case of Java) having lots of error handling boilerplate all around a function.
In Java of course this doesn't have to happen, but it can end up happening if you chose to have many exception types and different exceptions types when crossing layers (e.g. try { doX(); } catch (IOException e) { throw new MiddleLayerException("failed to do X", e);}).
To be fair though, Java still allows you to write that like this:
try{
x = doX();
y = doY(x);
z = doZ(y);
} catch (XException | YException | ZException e) {
throw new MiddleLayerException("...", e);
}
Which is still better than Go's:
x, err := doX();
if err != nil {
return MiddleLayerErr(err);
}
y, err := doY();
if err != nil {
return MiddleLayerErr(err);
}
z, err := doZ();
if err != nil {
return MiddleLayerErr(err);
}
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?