5 ms·
The right way isn't Either / Option (though it's a lot more viable with top-level type inference); it's unchecked exceptions. Checked exceptions have bimodal u
by barrkel 6y ago
The right way isn't Either / Option (though it's a lot more viable with top-level type inference); it's unchecked exceptions.
Checked exceptions have bimodal usage, from the programmer POV. Either you care about the exception, and you deal with it very close to the throw point, or you don't care about the exception, and it should be handled far far away, across many stack frames.
The former isn't a problem. It's the right thing if e.g. you try to open a file and the file is missing, and you have reasonable error handling logic to retry, open a different file, replace the file and try again, whatever.
The latter is where the issue lies. If you're handling errors far away, then you're handling lots of different errors there, and you're not distinguishing between them, because there's too many different failure modes. You're most likely just in a loop logging errors, or terminating. You're too far from the cause of the exception to do anything specific with it, the context is lost. So the effort to transport the exception type throughout the call graph is pointless.
tl;dr: checked exceptions are fine near the leaf of the call graph, but are increasingly pointless towards the trunk.
- xienze 6y ago> You're too far from the cause of the exception to do anything specific with it, the context is lost. Which is perfectly fine. If you _can_ handle the exception near where it happened, do so. Otherwise bubble it up and log it or whatever. Consider a REST service. If a DB or other error happens, I can retry right then and there if business logic calls for it (probably not), or just kick the can down the road where something appropriate like a 500 response serializer will take care of it. That’s a great pattern in my opinion.
- overraskning 6y agoI think you're making a good point here if you look at it as an application developer. In my experience, checked exceptions are helpful when using a library. Would you recommend this approach for libraries as well or do checked exceptions have more of a role to play there?
- ragnese 6y agoSo you're saying checked exceptions are perfectly fine. I'll write my library code with checked exceptions. You call my library code in one of your methods. The compiler tells you to do something about the possible failure. If you want to handle it there, you handle it with a try{}catch{}. If you don't, you wrap it in a RuntimeException and rethrow it so your top level handler can deal with it. Perfect. Unchecked exceptions make it easy to fuck up the case where you actually might want to handle it close to the call.
- chriswarbo 6y ago> If you're handling errors far away... the context is lost. So the effort to transport the exception type throughout the call graph is pointless. I get what you're saying, and things like 'Try[T]' lose the specifics of the error type too (we just get a 'Failure(Throwable)'). My problem occurs earlier on: which code might throw exceptions, and why? Java's checked exceptions let methods declare "I can fail with an AccessDenied error (along with all the usual stuff like NullPointerException, etc.)", and the compiler will make sure that's handled somewhere (even if it's just a log and quit). Unchecked exceptions are implicit; the signatures don't tell us they exist, and the compiler doesn't check that they're handled. This makes sense as a last resort, for things like OutOfMemory (although Zig would disagree!); but in general it's unhelpful and dangerous. I think it's a poor choice on Scala's behalf to treat all exceptions this way. Their only redeeming feature is short-term convenience; it's essentially an instance of static versus dynamic typing. "Exception polymorphism" (which I imagine would look something like row polymorphism) would make checked exceptions more convenient, since we wouldn't need exception-specific boilerplate, and this might be enough to solve the problem. Maybe the JVM might gain such a feature in the future, but until then we can use 'Either' or 'Try' to achieve a similar thing: they show us explicitly which methods can fail (answering the question in my second paragraph), and they force us (via the type checker) to handle the error case somewhere (even if that's just a generic log+quit handler at the top level, as you say).
- dpc_pw 6y ago`Result<T, dyn Error>` from Rust (since I'm not a Haskeler) is a result that can contain any error. Which is exactly what unchecked exceptions are. Rust developers can fluently switch and convert between conrete `Result<T, SomeErrorEnumeration>` and `Result<T, dyn Error>`. It works beautifully. Libraries usually enumerate their errors (leafs), applications usually just throw everything to one universal error bag.
- chriswarbo 6y ago> Rust developers can fluently switch and convert between conrete `Result<T, SomeErrorEnumeration>` and `Result<T, dyn Error>` That's something different, since both cases tell you that errors might occur. In Java we can do the following: public int concreteChecked() throws FileNotFound { if (bar) throw new FileNotFound(); return 42; } public int polymorphicChecked() throws Exception { if (bar) throw new FileNotFound(); return 42; } public Either<FileNotFound, int> concreteEither() { return bar? new Left(new FileNotFound()) : new Right(42); } public Either<Exception, int> polymorphicEither() { return bar? new Left(new FileNotFound()) : new Right(42); } public int unchecked() { if (bar) throw new RuntimeException(new FileNotFound()); return 42; } I think your 'Result' examples are like the third and fourth examples above: using a sum type, differing by whether the error is more/less specific. The first and second use checked exceptions, again differing in whether the error is more/less specific. Importantly: these will refuse to compile if we don't have 'throws ...' in their signature. The last example uses an unchecked exception: if we throw 'RuntimeException' (or a subclass), we don't need to put 'throws ...' in the signature, and hence the compiler won't keep tell us to put 'catch' block anywhere.
- dpc_pw 6y agoIn Rust you can do `Result<T, dyn SomeErrorInterface>` as well, which makes it able to express everything exactly like Java can. It's just Rust community generally doesn't bother with the taxonomies of errors. You either get a concrete list, or "any error".
- hota_mazi 6y agoUnchecked exceptions are the worst of both worlds: you can call a function that will throw an exception that you could recover from but you have no idea, so you don't even know you should catch it. At least, Either/Option force your code to take errors into account. You still have to bubble them up and compose them manually, though, which is why checked exceptions shine.