6 ms·
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 writ
by crabbone 3y ago
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?
- invalidname 3y agoNo. 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.
- naasking 3y agoChecked exceptions should be the default, exceptions should just be rare. They are overused in almost all languages.
- invalidname 3y agoWhat about: NullPointerException? ArrayIndexOutOfBoundsException? IllegalArgumentException? UnsupportedEncodingException?
- naasking 3y agoWhat 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.
- invalidname 3y agoThey aren't simple to express and propagate? What's so hard about declaring throws?
- naasking 3y agoYou 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.
- invalidname 3y agoThat's great, think about it... 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();
- tremon 3y agoWhat 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?
- deleted 3y ago[deleted]
- naasking 3y agoSituations in which you must raise, propagate and handle exceptions should be rare.
- eptcyka 3y agoParsing 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.
- naasking 3y agoReturn values obviously, with suitably designed method contracts. Some (maybe most) types of I/O errors are proper exceptions. Parsing failures are not.
- tremon 3y agoSo 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?
- naasking 3y agoThey 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.
- matwood 3y agoAgreed. Too often exceptions end up driving normal control flow, when the should just be...exceptions.
- ajanuary 3y agoYou must have a real panic when you see a sign saying “No parking except weekends.” Weekends happen all the time!
- theshrike79 3y agoSo if you write code that handles dates you'd throw NotWeekendException to notify the caller that it's not weekend? Exceptions should be for exceptions, not control flow.
- kelseyfrog 3y agoI'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.
- naasking 3y agoHe 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.
- kelseyfrog 3y agoHmmm, sounds like it just transforms into a Sorites paradox, no?
- 3y ago
- amalcon 3y agoThe 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.
- naasking 3y agoI think there are idioms where your unchecked exceptions can be caught, like via some scoped context, like an actor.
- amalcon 3y agoYes, 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.
- astral303 3y agoDeclaring 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.
- jayd16 3y agoYou 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?
- invalidname 3y agoI 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.).
- crabbone 3y ago> 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.
- invalidname 3y agoIf 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.
- dimgl 3y ago> 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.
- tsimionescu 3y agoThe 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); }