Settings

Theme

Unchecked Java: Say goodbye to checked exceptions

github.com

143 points by rogerkeays 2 years ago · 296 comments

Reader

breadwinner 2 years ago

One of the biggest flaws in C#, in my experience, is lack of checked exceptions. As an example, I wrote some very good 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.

More on checked vs unchecked exceptions here: https://forum.dlang.org/thread/hxhjcchsulqejwxywfbn@forum.dl...

  • jabiko 2 years ago

    Lets be honest. The more likely thing is that either the coworker would use an unchecked exception or that they would change the callsite to:

      try {
        theUpdatedFunction();
      } catch (MyNewCheckedException e) {
        logger.warn("Whoopsie doopsie", e)
        throw new SomeUncheckedException("Something failed, idk", e)
      }
    
    Which really is a zero sum game. The code still breaks the same way, but the checked exception gets eventually wrapped in an unchecked one. We still would have the situation that someone changed the behavior of the function in a way that is incompatible with your usage.

    That bug should have been caught by tests, code reviews and good communication.

    • phillipcarter 2 years ago

      Yep. This is similar to a viewpoint by Anders Hejlsberg and many on the C# team at the time: https://www.artima.com/articles/the-trouble-with-checked-exc...

      > You see programmers picking up new APIs that have all these throws clauses, and then you see how convoluted their code gets, and you realize the checked exceptions aren't helping them any.

      And he goes on to elaborate on how this gets more complicated when versioning is introduced into the mix.

      • breadwinner 2 years ago

        Hejlsberg is a terrific compiler writer. Turbo Pascal was awesome! He is not a good language designer, however. The exception mess in C# is the proof. See my comment at the root of this thread.

        • phillipcarter 2 years ago

          Yes, not a good language designer responsible for (checks notes) one of the most-used and most-loved programming languages on the planet.

          • interlocutor 2 years ago

            Do you mean Turbo Pascal? He didn't design it, Niklaus Wirth did. Or do you mean C#? James Gosling designed most of that (C# got its start by copying 90% of Java). Do you mean TypeScript? That's mostly JavaScript.

        • blackoil 2 years ago

          I would say he is one of the better language designers. Unlike many language designed by theorists and academics, he understands how the engineers are using the language and how a feature is used/abused and develops around it.

        • bheadmaster 2 years ago

          I doubt you can evaluate man's skill through a single project. It's not nice to use ad hominem to advertise your own comment.

          • 411111111111111 2 years ago

            Their comment was the one that this thread is bases on. They're merely pointing out that the original comment already gave an example why its a mess.

            I disagree with their point though, in java the lib would've probably just converted the exception to a runtime exception so the API doesn't change...

    • breadwinner 2 years ago

      > That bug should have been caught by tests, code reviews and good communication.

      There's a reason we have compile-time checks. If you think compile-time checks should be replaced with additional tests, code reviews and good communication, then you want a scripting language, not a compiled language.

      You do not wrap checked exceptions in an unchecked one... unless you are a really bad Java programmer.

      • insanitybit 2 years ago

        > unless you are a really bad Java programmer.

        Here's the thing. You've just described the vast majority of programmers - bad. They're really fucking bad. And language constructs that lead to bad programmers doing stupid things are, unfortunately making things worse for everyone.

      • hinkley 2 years ago

        I've given a lot of time and benefit of the doubt to 'testing replaces static analysis' and not only do I know this is not true, but I'm also pretty sure that testing is fundamentally broken. It can't fix anything because it can't even fix itself.

        Maybe a new testing paradigm will fix it, I certainly keep an eye out for them, but nothing so far. Property based testing is better, but mostly it just reminds me that we had Design By Contract 30 years ago.

      • colanderman 2 years ago

        > You do not wrap checked exceptions in an unchecked one... unless you are a really bad Java programmer.

        I disagree -- this is the correct thing to do if you believe it is not possible for the checked exception to occur. (Catching it is wrong -- what would you do to correct something which you believe not to be possible? Forcing the caller to handle it is wrong -- if you don't know what to do with it, they sure won't!) Wrapping checked as unchecked encodes your belief that should it occur, it is a logic error, akin to out-of-bounds array access or null pointer dereference.

        (Of course, swallowing expected exceptions one is simply too lazy to do anything about is poor practice! Not disagreeing with that.)

        • rixed 2 years ago

          > if you don't know what to do with it, they sure won't!

          Actually no. For instance, the caller at some point up the stack may know if something is worth retrying.

          • colanderman 2 years ago

            Then you should unwind your state correctly and rethrow as a checked exception stating as much. If there levels up the stack I see a random unexpected "IO Exception" from your library I have no idea whether you are still in a valid state to retry. If I instead see a "Foolib Request Aborted: reason IO Exception" I know you've written logic to handle that case, and I know that retrying is appropriate.

        • Hermel 2 years ago

          > I disagree -- this is the correct thing to do if you believe it is not possible for the checked exception to occur.

          If it is not possible to occur, then it should not be part of the API.

          The only time I rethrow a checked exception as an unchecked exception is when the code is still under construction. The default of the eclipse code generator is to log and ignore caught transaction. I think wrapping into an unchecked one is the better default behavior for incomplete code under a "fail fast" policy.

          • colejohnson66 2 years ago

            > If it is not possible to occur, then it should not be part of the API.

            Ah, but what if it can occur, just never with what you pass in? Suppose a function is documented to throw some checked exception if some parameter is a negative number, but you pass in a positive literal/constant? In such a situation, the checked exception will never occur! With Rust, for example, this is easily done with an `unwrap()` (and, possibly, a comment) to assert said belief, but with checked exceptions, there's no way to force the compiler to squash the required check.

          • colanderman 2 years ago

            Example: validation of serialized data followed by deserialization. Deserialization properly should throw a checked exception, since invalid serialized data is very much a thing. But in this case the serialized data is known to be correct (because it passed validation). The checked exception will never occur, and were it to, there's nothing we could do about it, because it reflects a logic error, same as out of bounds array access.

            The algebraic data type equivalent of this shows up all the time in functional code -- unwrapping a result type you know can't be Error/None/etc. because of logic. You don't rewrap it and force the caller to deal with a case which logically cannot occur; instead you throw an unchecked exception.

      • erik_seaberg 2 years ago

        stream.filter(p) requires wrapping checked exceptions in p, or altering the language so that they’re no longer checked. Same with Runnable.

        • masklinn 2 years ago

          You could also alter the langage woth for exceptions transparency (swift has something like that with “rethrows“).

          I don’t think that’s close to sufficient to making checked exceptions work tho, let alone good.

        • chii 2 years ago

          or you start doing monads instead, where p operates on a monad (aka, the Optional type).

          Unfortunately, this ends up propagating out and your entire code base needs to also do this. Tho i reckon it will make your code better in the future, for a short term/temporary pain converting/adding breaking changes etc.

      • sobellian 2 years ago

        If this were 100% true, everyone would want a language for formal verification (Idris, Coq, etc). Short of those lofty heights we can quibble over the varying strictness of Haskell vs Java vs C and so on.

        • edejong 2 years ago

          A good programmer knows the limits of the type system. They lean on that knowledge to determine which tests to write.

      • smrtinsert 2 years ago

        > You do not wrap checked exceptions in an unchecked one... unless you are a really bad Java programmer.

        That's not a given. It's perfectly fine to handle a checked exception via some custom runtime. The nice part is your your code base was given a chance to handle the key exceptions of this particular api all from your ide without you having to refer to documentation etc.

      • pestatije 2 years ago

        i do...if i ever get an IOException all i have to do is end the app, no need to deal with it in any way different than an unchecked exception

        https://phauer.com/2015/checked-exceptions-are-evil/

        • colanderman 2 years ago

          A better example for IOException is network errors. If your socket dies, your code should attempt to reopen it! It should report to the user if it can't!

          Moreover, it's good that code nearest the site where the exception is thrown handles the error, as only it has context for what's going on at the time. Code further up the stack won't have any clue what this random IOException might relate to.

          If you're confident the IOExceptions can't occur under normal conditions -- say, you know the file has correct permissions, isn't being concurrently modified, etc. -- then, encode this belief by catching the IOException near to its origin and rethrowing as an unchecked exception.

          This same pattern shows up even in languages without exceptions. In C -- always check errno; don't try to catch SIGSEGV or SIGABRT; raise SIGABRT if errno is something you don't plan to handle. In F# -- you're forced to match Ok/Error; don't try to catch exceptions; raise an exception if you don't expect Error.

          • rrobukef 2 years ago

            But with abstractions you can have many more functions not handling the error before you get to the actual context.

            Is it worth it to thread the IOException through the network layer, then the HTTP-client library, then then business serialisation up to the actual context?

            I don't want a dozen checked exceptions. (and with this approach socket-limitations and connection loss should get a different exception.) I also don't want catch and rethrow. Unchecked+checked+docs+crashes are a measured approach.

        • breadwinner 2 years ago

          Check the root message of this thread... if you use unchecked exceptions you either crash or you swallow all exceptions. Both are evil, and checked exceptions are the solution.

          • colanderman 2 years ago

            That's only possible if your code is 100% correct.

            Sometimes, your logic is flawed. A condition occurs which you erroneously deduced not to be possible.

            Unchecked exceptions are the necessary manifestation of these unforeseen errors. Catching them is pointless, what will you do with them? Dynamically fix the logic of your program? What can be the reasonable response to "index out of bounds", try a different index? [1]

            Depending on the domain it may be appropriate to convert them indiscriminately to checked exceptions at module boundaries (e.g. requests within a server) -- but within said module it remains pointless to catch them. (This is a form of swallowing.)

            In other domains, crashing is the correct behavior. The code cannot proceed correctly, it must abort.

            Checked exceptions are appropriate only when the exception can be anticipated and thus planned for, ideally by code closest to where it is thrown.

            [1] Actually there was a paper a long time ago that monkey-patching C code to return fake "null" data in response to out-of-bounds memory accesses actually resulted in the intended (i.e. correct) behavior in the majority of cases. But I digress.

            • breadwinner 2 years ago

              Some errors are recoverable, some are not. When recoverable errors are possible you want to know what those errors are. If you don't then you will crash when you could have recovered. And that's the reason for writing down the list of possible exceptions where recovery and retry are possible (aka checked exceptions).

              When recovery should not be attempted (example: "index out of bounds") then you don't want to declare or catch the exception, and that's when you use a subclass of RuntimeException in Java.

    • doliveira 2 years ago

      If the Future, Executor, Optional, Streams, etc played nice with checked exceptions it would probably be different. Right now at some point you do have to wrap it into an unchecked exception.

      • throwaway2037 2 years ago

        I agree. It is weird that for interface Runnable there isn't a ThrowingRunnable. I always create them in my projects and make my callbacks use them along with ThrowingConsumer and ThrowingProvider.

    • hinkley 2 years ago

      Let's be really honest

        try {
          theUpdatedFunction();
        } catch (MyNewCheckedException e) {
          logger.warn("Whoopsie doopsie", e);
          throw new SomeUncheckedException(e.message);
        }
      
      There's a special place in Hell for these people, but at least they'll get to see all of their friends again.
      • mdaniel 2 years ago

        Oooo, lookit you working with professionals; I've seen waaaaaaay too many instances of this shit

          try {
            somethingImportant();
          } catch (Exception e) {
            // can't happen
          }
        
        IJ finally started chirping about this other anti-pattern

          try {
            doit();
          } catch (Exception e) {
            System.err.println("something didn't work");
            // failure to reference "e" is now a yellowbox
          }
        
        I have to constantly ask for them to either log it, or rename the variable to `ignored` to get IJ and future readers on the same page that it was intentionally swallowed
    • coldtea 2 years ago

      >The code still breaks the same way

      That's not a valid argument. This is an explicit decision, so they reap what they sowed.

      One could just as well make the same argument for Optionals, that you can just do:

        let val = foo.unwrap();
    • __jem 2 years ago

      Or, worse, you're reviewing a large diff, see the logger statement but don't notice that the junior whose code you're reviewing forgot to rethrow. Now you've accidentally entered uncharted territory where way more scarier things can happen to your application than just crashing. Of course this shouldn't happen, but it does.

    • bedobi 2 years ago

      > That bug should have been caught by tests, code reviews and good communication

      This is such an unproductive cop out response. The problem could be completely solved by functional error handling, which leverages the compiler to force you to handle exactly the errors than can happen, no more, no less.

    • hourago 2 years ago

      > throw new SomeUncheckedException("Something failed, idk", e)

      Who does that? It looks like an anti-pattern.

  • wvenable 2 years ago

    > One of the biggest flaws in C#, in my experience, is lack of checked exceptions.

    I couldn't disagree more. Checked exceptions in Java have ruined a generation of programmers.

    The truth is, under checked exceptions, to satisfy the compiler the function that you called would declare that it throws a SomeModuleException and the programmer who wrote that function would put all his code in try/catch block that catches all errors and rethrows a SomeModuleException with the real exception passed as the cause.

    Checked exceptions just don't work.

    • com2kid 2 years ago

      From a type theory perspective, an exception is another type of return value that a function can have, and not documenting those in the function signature is absurd.

      I have seen libraries that make network calls that do the following:

      * Throw an exception on certain types of network errors * Return an HTTP error code for other types of network errors * Return an object with error set to true and an error string for other types of network errors

      NONE of those was documented, it is crazy. This actually bite me in prod when an undocumented exception that we hadn't seen in over a year of uptime was finally thrown. I had to look at network logs to try and figure out why it was being thrown (no checked exceptions in JS!), which itself is absurd.

      • jakewins 2 years ago

        I think the issue is with checked exceptions as a solution to how to declare this signature, or at least with how they are designed in Java: if you add a new exception to the signature, every downstream call site needs to change, recursively.

        This means it is impossible to add new exceptions to library methods without breakage, and even in your own code it may mean hundreds of changes throughout your code to add a new exception type.

        Rusts solution seems to have improved this conundrum, where you can describe how to map the new error into an existing hierarchy in a single or a few places instead of at every call site

        • kaba0 2 years ago

          > This means it is impossible to add new exceptions to library methods without breakage

          That’s a feature, not a bug. If it wouldn’t, an exception of that kind could bubble up at a place you didn’t expect.

          Also, it doesn’t require that many changes, it only has to be changed up to the point where you intend handling it.

          I don’t see how rust would be immune to that.

          Though it is true that polymorphism with respect to checked exceptions would be great. The new generation of languages might have that (e.g. Koka)

          • wvenable 2 years ago

            > If it wouldn’t, an exception of that kind could bubble up at a place you didn’t expect.

            You should always be able to handle an unknown exception. Sure you can't do much about it, but it shouldn't be a big deal. An exception only occurs if the code can't continue as expected.

            Libraries are supposed to be a point of abstraction. They should be allowed to change their implementation fundamentally as long as they continue to respect the same interface. Exceptions should not be part of the interface explicitly because are they implementation-detail related.

          • nradov 2 years ago

            If you don't control the code where the superclass or interface is defined then you're stuck. You can't change that code even if it would be only a tiny edit. Checked exceptions are absolutely not a "feature".

      • WorldMaker 2 years ago

        From a type theory perspective if you want to track exceptions as return values you want a composable container class (best case, a full monad) so that you aren't writing a lot of ad hoc code every time to handle them.

        That's a big problem with Java's checked exceptions, it's not very composable and flow control can just stack up into deeper and deeper "waterfalls" of code rather than simpler pattern matching. (To be fair that's a big problem with exceptions in general as "flow control", they aren't very composable.)

        Something like an Either monad in a language with sum types can be a great way to describe errors/exceptions usefully as return types. From a type theory perspective, checked exceptions can be seen as a hack for a language that doesn't support sum types and doesn't have great native monad binding.

      • mrkeen 2 years ago

        > and not documenting those in the function signature is absurd

        Exceptions can be thrown from anywhere. That's the documentation.

    • breadwinner 2 years ago

      That's how you do it in C#... See InnerException [1]. What a good programmer would do is to throw a new exception, and fix the callers to handle the new error.

      https://learn.microsoft.com/en-us/dotnet/api/system.exceptio...

      • wvenable 2 years ago

        That assumes you have control over the callers. If you're writing a library, you don't. Checked assumptions are incompatible with polymorphism and even just basic abstraction afforded by functions. One can't arbitrarily change an implementation without changing the signature and breaking all the callers.

        The vast majority of cases, direct callers can't properly the handle the new error anyway. Callers either have to eat that exception, convert it (thus defeating the purpose of checked exceptions), or change their signature. Good programmers will change the signature and now every one of the callers of those methods have do the same thing. Ad infinitum.

        • breadwinner 2 years ago

          You can have multiple implementations of database drivers, but they all throw a subclass of SQLException. So yes, polymorphism is certainly possible. And when you get a SQLException (for example SQLIntegrityConstraintViolationException), you don't want to just crash, or display whatever error message the database gave you... you want to, at the very least, provide guidance to the user on what corrective action to take. And in some cases the program can automatically take corrective action. And that's the reason for writing down a list of possible exceptions where recovery and retry are possible (aka checked exceptions).

          • wvenable 2 years ago

            That's an extremely contrived example. What if now you want to do a NoSQL solution? Or a flat file?

            But really the problem is actually far worse than that. Checked exceptions are brittle under any kind of implementation change. If you have a function that calculates a rate just using arithmetic. But tomorrow it loads a flat file. And next week it uses SQL. And in a month it calls a web service. As a consumer, this is none of your concern -- that's the whole point of abstraction. It's even worse when dynamic (polymorhphism) or functional (external code calls you).

            What's the recovery from a SQLException anyway? How does that give you enough detail to do anything? You say SQLIntegrityConstraintViolationException but that's not the same type. Why does it matter that you declared it a SQLException over just Exception in that case?

    • jitix 2 years ago

      You’re both right and wrong. Checked exceptions slow down development, make code ugly and in my theoretical opinion are an anti-pattern that should never be used.

      However in small to mid sized enterprise software companies with average developer talent it’s important to keep boundaries (and blame) clear.

      In the scenario in question, the CTO will blame OP and make them work the weekend to diagnose/fix it, so wrapping the other exception and throwing their own SomeModuleException will cover their ass.

      Java is popular for all the wrong reasons. Believe it or not there are hundreds of such companies in the US alone heavily using Java in this manner and then there is the whole offshore development segment.

      • rogerkeaysOP 2 years ago

        I put Java's popularity down to good support for Corporate-Oriented Programming ;)

      • adamwk 2 years ago

        But even with unchecked exceptions, the exception value or description will still show up in the logs. I don’t understand how checked exceptions save you from a tyrannical CTO

        • jitix 2 years ago

          Good point, but in my experience siloed teams (often if different continents) usually don’t standardize on error messages and codes, and a vague message like “Invalid name” doesn’t indicate which layer the message is coming from.

          From operations’ point of view the bug will be pinned on the owner of the service that is returning the JSON to the client/app and it’s up to them to trace it down the layers. A hierarchy of wrapped exceptions helps with that. Kind of like saying “I can’t do it because XYZ didn’t do what they were supposed to do”. So low key corporate blame game in code.

          I know that the usual HN crowd doesn’t work at/know about companies that follow this, but this pattern is way more prevalent in the broader “IT” industry esp among offshore centres.

  • __jem 2 years ago

    On the other hand, in lots of application code, there's often not much to do other than crash or abort the request, and this can often be safer than trying to proceed along code paths that are almost always under-tested. We've all seen Java code where a junior has decided to swallow an error without much thought. This obviously isn't true for every use case, but failing fast is often a pretty good policy.

  • h4x0rr 2 years ago

    Checked exceptions just always felt annoying to work with. Imo Rust's Result type is more versatile

    • wvenable 2 years ago

      Checked exceptions are just another return type with a weird syntax. It's not that Rust's result type is more versatile, it's just less syntax to accomplish what works out to be the same thing.

      • jcparkyn 2 years ago

        The difference is more than just syntax, at least in the case of Java and Rust. The core problem with checked exceptions in Java (as demonstrated in most of the examples from TFA) is that they don't compose properly with generic functions like map. Checked exceptions exist in parallel to the type system, and other parts of the language don't have the capacity to deal with them.

        On the other hand, a Result type makes errors part of the type system, and all other code can work with them "by default".

        • int_19h 2 years ago

          That's a specific choice that Java designers made, though, not something inherent to checked exceptions in principle. During Project Lambda, one of the proposals - Neal Gafter's - actually had a full-fledged generic exception specification facility that allowed you to define HOFs along the lines of "I throw everything X does, and also Y". They killed it because it was "too complicated".

        • wvenable 2 years ago

          I agree. Checked exceptions are just a worse version of Result types. I did not intend to make them sound better than they are.

          Unchecked exceptions, however, are a different beast entirely. They're not just checked exceptions without the checking; they fundamentally change how you code and how you think about error handling.

        • ben0x539 2 years ago

          Whoa, I've been having opinions about error handling and result types and checked exceptions for like years and I never considered that Java couldn't be "properly" polymorphic over checked exception types. Thanks for pointing that out.

        • benjiweber 2 years ago

          You can use checked exceptions equivalently to Result types in streams e.g. https://github.com/unruly/control/blob/master/src/test/java/...

  • taeric 2 years ago

    I mean, maybe? Fun degenerate cases to consider: Someone throws a Environment.Exit(0) into a random library you are using, instant pain. Someone throws an infinite loop into a library you are using, similar instant pain.

    There is no magic language trick that can prevent you from having to rerun all tests for your software if you update a dependency. Pretty much period.

    (I say this as someone that isn't really opposed to checked exceptions.)

    • anon-3988 2 years ago

      > There is no magic language trick that can prevent you from having to rerun all tests for your software if you update a dependency. Pretty much period.

      Of course, but having a checked exception (even better, having the errors be part of the return type via Result<T, E> or Option<T>) solved an entire class of problem. Doesn't mean that there aren't others of course. But surely this is a win?

      >inb4 it makes the code very complex with nested return type Result<Result<Result<T, FileNotFoundError>,ReadError>, ParseError>

      Then it was just hidden from you before. The complexity had always been there, it just never occurred to you that it can happen.

      • masklinn 2 years ago

        > But surely this is a win?

        Not when it creates more issues than it solves, which checked exceptions do.

        Checked exceptions are an entire side channel to the type system which breaks any sort of composition or genericity.

        Maybe this is solvable, but Java seriously poisoned that well because its implementation is so shit, and if you’re looking for this static safety, first the rest of a Java-style type system does not justify it (there’s so many low hanging fruits), and second a result-style things will already give you the same benefits in a form which is known to work.

      • taeric 2 years ago

        Ish? It is all too often that, at the point where you would be forced to catch the exception, the best path forward is to signal it to the user and see if they have a plan. Unfortunately, the only mechanisms we allow for that in modern systems is to unwind the entire stack to get to the call that came from a user. And whether done with chained return values, or with exceptions, the code will get to be a mess.

        Signals that don't unwind can have their own problems, of course. I don't mean that as a silver bullet. But all too often the exceptions and error conditions that we use to teach these ideas are far more difficult because of our insistence on unwinding the stack. Neither return values nor exceptions change that.

    • breadwinner 2 years ago

      If someone added an extra parameter to a function you're calling, when would you want to be alerted about it? At compile time? Or when you rerun all the tests?

      Hopefully, your answer is compile time. If so can you now understand why you would want to be alerted about a new exception getting thrown at compile-time as well?

      • taeric 2 years ago

        I'd like to think that, as an industry, we would move to libraries being foundational code, such that they don't change in fundamental ways. If a parameter is added, it should be done in a backwards compatible way, such that I don't have to know about it. The old behavior would continue to work.

        As such, any breakage from calling would be, by definition, a bug. And no, we have not found a way to prevent bugs.

      • mrkeen 2 years ago

        > If so can you now understand why you would want to be alerted about a new exception getting thrown at compile-time as well?

        The library code can already throw anything. OutOfHeap, over/underflow, div0, stackoverflow, threadinterrupted. The caller already knows the function can throw, and documenting one more flavour of throw doesn't tell the caller anything.

        • twelve40 2 years ago

          > documenting one more flavour of throw doesn't tell the caller anything

          but it does, in cases when there are errors that can or should be retried. Like a whitelist of documented cases where recovery and retry are possible, and of course all this infinite runtime stuff that can happen unexpectedly, for which there is no immediate solution.

  • nradov 2 years ago

    The trouble with checked exceptions is that they prevent you from easily extending classes or implementing interfaces that you don't control. Your new class might need to throw a checked exception not included in the method signature. So then you have to resort to hacks like wrapping the new checked exception inside a runtime exception.

    • breadwinner 2 years ago

      Not true. Exception hierarchies solve this problem.

      • nradov 2 years ago

        Nope. Exception hierarchies don't even come close to solving that problem because the superclass or interface author usually didn't anticipate your need and define the method signature in a way that would be useful with exception hierarchies.

        • breadwinner 2 years ago

          So you're inheriting a class not within your own application, but written outside of your application. Composition might work better in this case.

          • nradov 2 years ago

            Nope. Composition doesn't help when you have to implement an existing interface in order to make some API work.

            • int_19h 2 years ago

              If you're implementing an existing interface for that reason, it's because something else is going to be calling you. In which case throwing an exception that they don't expect is probably a bad idea, and relying on it just flowing through their code back to you is basically relying on implementation details in many cases.

              • nradov 2 years ago

                No, that's not how it usually works in practice. Typically where this becomes a problem is in using an existing library as a middle layer in your application code. So you could catch your own additional exceptions, but the interface method signatures don't allow for that. Hence the need for ugly hacks like wrapping the checked exception in a runtime exception, then catching that and extracting the original exception. A real mess, but still better than the alternative of forking and modifying the library.

                • int_19h 2 years ago

                  That is exactly my point - when you have a middle layer written by someone else sandwiched between parts of your code, assuming that any random exception you can throw will flow through (or, for that matter, that there even is a "through" in many cases - e.g. if the library makes some part async) - is relying on implementation details of said code.

  • sonicgear1 2 years ago

    You should catch any non specific exception in the last catch block, just in case. Thought that was the standard. Java gets very messy in mixing checked and unchecked exceptions, I've seen a lot of devs just ignore the unchecked ones.

    • breadwinner 2 years ago

      So you mean, catch the root Exception class. Pretty much everyone agrees that's a bad idea, C# and Java both advice against that. But in the case of C# you have to do it, to avoid crashing all the time, because there's no reliable way to determine what exceptions can be expected. In Java the compiler will tell you what exceptions can be expected, but in C# you have to rely on documentation which is not reliable.

      • yardstick 2 years ago

        Not just Exception. Throwable.

        I’ve seen too much code were some random problem in an Error - OutOfMemoryError for example when processing too much data (Eg call data records, payment records, analytic records, whatever). If it’s a batch processing job, you don’t want this problem for this specific entity causing the rest of your reports not to be sent.

        I’ve also seen stupid things like RPC libraries silently swallow Errors and not report them properly to the caller, so we end up wrapping all RPC server endpoint methods in a try-catch-Throwable just so we can see the problem and log it.

    • int_19h 2 years ago

      And do what with it? If you don't know what it really is, the best thing is to let things fail fast and the standard logging to kick in and record all the details of the crash.

      • yardstick 2 years ago

        Depends on what you are doing.

        If I’m processing a monthly report for millions of customers, I don’t want to abort processing after running into one problem customer. I want to continue to process the rest of them, and log the problem customers exception for troubleshooting and analysis offline.

        • nradov 2 years ago

          Depending on what actually happened, that could be disastrous and result in data corruption or account reconciliation failures or worse. It often would be safer to go ahead and fail even in that circumstance.

  • skissane 2 years ago

    > 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.

    Not necessarily. If it started throwing a new RuntimeException, it wouldn’t have. There are also sneaky ways to throw checked exceptions without declaring them, for example using Lombok’s @SneakyThrows annotation

    • breadwinner 2 years ago

      Certainly you can intentionally break it, but then that's on you.

      • skissane 2 years ago

        A person can also accidentally break it - by adding some new code which contains a bug which causes an unchecked exception or error to be thrown (e.g. NullPointerException, ArrayIndexOutOfBoundsException, StackOverflowError, etc).

        Checked exceptions do nothing to protect against those kinds of mistakes, which in my personal experience are vastly more common than whatever mistakes for which they may provide some protection

        • kaba0 2 years ago

          Everything can fail at every time — that’s part of the art of programming to discern which error conditions are meaningful to be included in the signature, and which are not.

          • skissane 2 years ago

            > Everything can fail at every time — that’s part of the art of programming to discern which error conditions are meaningful to be included in the signature, and which are not.

            But that's a major flaw with checked exceptions – very often, whether an error is recoverable or not depends, not on the API itself, rather on how it is used. Yet checked exceptions force the API designer to make that decision while designing the API, when they can only guess at how it will be used.

            A good example of this is FileNotFoundException – whether that is a recoverable error which ought to be handled, or whether there is nothing better to do than crash, depends on what the file is. If we are implementing a File Open dialog box in a GUI app – okay, we better catch the FileNotFoundException and display an error box, not just crash. But, suppose I am writing a micro-service, and the first thing it does on startup is read its config file, and the config file isn't there: is there any point in trying to handle that exception, or should it just crash? Obviously the designers of Java's file IO classes had the first scenario in mind more than the second, but it is an inherent flaw of checked exceptions that they forced them to make this decision at all.

  • oh_sigh 2 years ago

    Your code might have been very good, but it wasn't future proof. It sounds like your code would have been fine if you locked down the libraries you were calling to a fixed version or something.

  • 29athrowaway 2 years ago

    In Rust instead of throwing, you have results that can be either successful or errors. And it is all type safe and fast.

  • joiqj 2 years ago

    One of the reasons I hate exceptions overall. Someone may add new ones, they break the control flow, they bubble up all over the place, etc etc. They are basically indomitable.

bedobi 2 years ago

Exception based error handling is so bad and unsafe that adopting functional error handling with Either, Try etc as implemented by functional addon libraries for many languages, while not yet common, in time it will become the new default even in OO languages. (just like it's been the default in functional languages for decades)

Functional error handling types are much simpler, safer and more powerful.

Simpler because they don't rely on dedicated syntax- they're just regular objects no different to any other object.

Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling)

Powerful because they support map, flatmap, applicative etc, making it easy to eg chain multiple computations together in desired ways, which is unwieldy and bug prone when using exceptions.

> What is wrong about dedicated syntax

It adds complexity to the language! It could be that, when learning Java, Kotlin and any other language, we learn that methods return what they say they do... and that's that. No weird dedicated syntax and magic, special treatment for returning anything other than the happy path, and the HUGE complexity that comes with it, eg the dedicated syntax itself and how it behaves, differences between checked and unchecked exceptions, hierarchies of exceptions etc etc.

> Exceptions are easier

But that's the point, they're not.

Exceptions based error handling is unnecessary, hugely complex, doesn't compose at all, obfuscates or straight up hides what can go wrong with any given call, so leads to countless trivially preventable bugs... I could go on. And after decades of use, there's still no consensus about what exceptions should be or how they should be used. Exceptions are a failed experiment and I have no doubt that in ten years, Java, Kotlin and many other languages will acknowledge as much and move away from it the same way Joda Time outcompeted and replaced the horrible Java date and time library.

  • kaba0 2 years ago

    Checked exceptions are exactly analogous of Result/Either types. They are just built into the language with syntactic sugar, automatically unwrap by default (the most common operation), can be handled on as narrow or wide scope as needed (try-catch blocks), does the correct thing by default (bubbling up), and stores stack traces!

    In my book, if anything, they are much much better! Unfortunately they don’t have a flawless implementation, but hopefully languages with first-class effects will change that.

    • titzer 2 years ago

      > Checked exceptions are exactly analogous of Result/Either types.

      No, they aren't.

      They are not compositional. You may want to write `f(g())` but there's no way to write the parameter type of `f` to make this work (in Java). That's because checked exceptions are an "effect" that would require extending the Java type system.

      • rcme 2 years ago

        How would you define f in a different language such that f(g()) worked? You couldn’t do that in Go, for instance.

        • titzer 2 years ago

          There's at least one language, Koka (https://koka-lang.github.io/koka/doc/book.html) that has effects in its type system. There are probably others. If you don't have effects, you could use union types.

        • hedora 2 years ago

          In rust, you write "f(g()?)", and make sure that your function body returns a Result<> with an error type that g()'s error type can be converted to.

          It works great. Also, note that f() doesn't care about the type of error returned by g(), and that it will be a compilation error if g()'s error type turns into something incompatible.

          Sadly, there are proposals to add exceptions to rust, and it seems likely they will be accepted, breaking error handling semantics across the entire ecosystem (even in new code, since exceptions are too hard to use correctly).

          • rcme 2 years ago

            What's the rationale behind adding exceptions?

            • emi2k01 2 years ago

              I'm not sure what they mean by adding exceptions.

              AFAIK, the only proposal related to exceptions is adding a `try` block that would scope the `?` operator to that block instead of the current function.

        • EspressoGPT 2 years ago

          > You couldn’t do that in Go, for instance.

          You can – by making f accept g's return types, including the error. This is even being done in the Go standard library: https://pkg.go.dev/text/template#Must

          • rcme 2 years ago

            That’s interesting, but consider two functions like func ParseInt(string) (int, error) and func Abs(int) int. You lose some composability when using errors over exceptions. The Rust solution mentioned elsewhere seems elegant.

        • clhodapp 2 years ago

          Simple! You just make f take in g's result type.

        • masklinn 2 years ago

          You could actually, iirc if f takes as many parameters as g returns it works out.

          In a better langage f would take a Result, and then it can manipulate that however it wants.

          Obviously you can also plug in adapters if you need some other composition e.g. g().map(f), g().and_then(f), …

      • kaba0 2 years ago

        Read my last paragraph.

        • the_gipsy 2 years ago

          So they are better... in a theoretical implementation.

          • int_19h 2 years ago

            A prototype of the Java compiler with composable checked exceptions existed at one point, so we know it's definitely doable.

    • dllthomas 2 years ago

      To my mind the big issue with existing examples of checked exceptions is that the language to talk about the exceptions is woefully inadequate, so it stops you from writing things that would be useful while dealing correctly with exceptions. The go-to example is a map function, which we should be able to declare as throwing anything that might be thrown by its argument. Without that we need to either say that map might throw anything and then handle cases that actually can't happen, suppress/collect exceptions inside map, or suppress/collect errors inside the functions we're passing to map, all of which add boilerplate and some of which add imprecision or incorrectness. It would also be good to be able to state that a function handles some exceptions if they are thrown by its argument. And all of this should be able to be composed arbitrarily. And... somehow not be too complicated. For usability, it should probably also be possible to infer what's thrown for functions that are not part of an external API.

    • unscaled 2 years ago

      No, unfortunately they are not. The problem is not with checked exceptions themselves, but with the other type of exceptions in Java.

      In languages that rely on Result/Either for error handling, you've got two types of errors: Typed errors (Result/Either) and untyped panics. Typed errors are supposed to be handled, possibly based on their type, while panics can be recovered from ("catched") but these are serious, unexpected errors and you're not supposed to try to handle them based on their type. Since typed errors generally need to be handled explicitly while untyped errors are unexpected, typed errors are always checked (you can't skip handling them), while untyped errors are unchecked (implicitly propagated up the stack if you don't do anything to catch them).

      Java has three types of errors:

      1. Checked errors, a.k.a. checked exceptions: (exceptions that inherit from Exception, but not from RuntimeException). 2. Unchecked application errors: exceptions that inherit from RuntimeException. 3. Unchecked fatal errors: exceptions that inherit from Error.

      These three kinds of errors live in a confusing class hierarchy, with Throwable covering all of them and unchecked application errors being a special case of checked application errors.

      Like everything else designed in the early Java days, it shows an unhealthy obsession with deep class hierarchies (and gratuitous mutability, check out initCause()!). And this is what destroyed the utility of checked exceptions in Java in my opinion.

      Consider the following example: We have a purchase() function which can return one of the following errors:

      - InsufficientAccountBalance - InvalidPaymentMethod - TransactionBlocked - ServerError - etc.

      You want to handle InsufficientAccountBalance by automatically topping up the user's balance if they have auto top-up configured, so you're going to have to catch this error, while letting the rest of the errors propagate up the stack, so an error message could be displayed to the user.

      In Rust, you would do something like this:

        account.purchase(request).map_err(|err| match err {
          PurchaseError.InsufficientAccountBalance(available, required) => {
            account.auto_top_up(required - available)?
            account.purchase(request)
          }
          _ => err // Do not handle other error, just let them propagate
        })
      
      In Java, you would generally do the following:

        try {
          account.purchase(request);
        } catch (InsufficientAccountBalance e) {
          account.auto_top_up(e.requiredAmount - e.availableAmount);
          account.purchase(request);
        } catch (Exception e) {
          // We need to catch and wrap all other checked exception types here
          // or the compiler would fail
          throw new WrappedPurchaseException(e);
        }
      
      The "catch (Exception e)" clause doesn't just catch checked exceptions now - it catches every type of exception, and it has to wrap it in another type! Of course, you can also specify every kind of checked exception explicitly, but this is way too tedious and what you get in practice is that most code will just catch a generic Exception (or worse - Throwable!) and wrap that exception or handle it the same way, regardless if it was a NullPointerException caused by a bug in code, an invalid credit card number.

      The worst problem of all is that once developers get used to write "catch (Exception e)" everywhere, they start doubting the values of checked exceptions: after all, most of their try clauses seem to have a generic "catch (Exception e)", so does it really matter at all of they're using checked exceptions?

      This is the reality. Checked exceptions failed in Java. Most Java developers see them as nothing more than a nuisance and look for ways to bypass them. That does not necessarily mean that the concept of checked exception as a language level facility for errors has failed, but it certainly failed the way it has been implemented in Java.

      • int_19h 2 years ago

        It's the ergonomics of it. For a checked exception facility to not be maddening, it needs to have good facilities to propagate and wrap exceptions easily and with minimal scaffolding. But for some reason no mainstream language with traditional exception handling did that.

        In a similar vein, it's ironic how often you hear "composition is better than inheritance" in OO design context, and yet how few OO languages have facilities to automate delegation.

        • unscaled 2 years ago

          > In a similar vein, it's ironic how often you hear "composition is better than inheritance" in OO design context, and yet how few OO languages have facilities to automate delegation.

          I wholly agree with this sentiment. Rust Result types where also excruciatingly inconvenient to use at the early days, but Rust gradually added facilities to make it better: first the try! macro and if-let, then the ? operator and finally let else. Together with crates like anyhow, thiserror and eyre, error handling became a lot better. I don't use Swift a lot, but it also seem to have iterated on its error handling.

          In the 27 years of its existence, Java did very little to improve exception handling facilities. It added exception chaining in Java 1.4 and catching multiple exceptions in Java 7, that's it. I'm not picking up specifically on Java here - I think many languages neglect exceptions or error handling. Go is also an instructive example of a language that chose a non-exception-based error handling mechanism that the designers claimed to be superior, but failed to add ergonomics to that. This is not for the lack of trying though: the Go team tried to fix this issue multiple times, but there are very vocal parts of the Go community who opposed any kind of ergonomics, in favor of "explicitness" (as if explicitness means "error-prone boilerplate"). I would give the Go team full score for seriously trying.

          I give them less score on the composition-over-inheritance part though. Go is one of the languages that has objects (structs) and interfaces, but disallows inheritance, but it doesn't provide any mechanism for automating delegation. Kotlin has shown that this is possible and even quite simple. It's not one of these languages features (like method overloading, type classes and generics) that carries a lot of corner cases and complexity that you have to deal with.

      • kaba0 2 years ago

        If all of those exceptions are custom made, then it makes sense to have a common subclass for them. It is not ideal, I agree, but it is hardly a showstopper.

  • misja111 2 years ago

    Exception based error handling is unsafe when they are unchecked exceptions. Checked exceptions however are as safe as Either, Try, Monads, Applicatives or whatever. You are forced to declare them in your method signature, the caller is forced to either handle them or rethrow them + declare them as well. And I guess this is precisely why so many developers hate them; they don't like the extra work they have to do to catch all those edge conditions. This is why we see so many empty catch blocks or upcasting to Exception or even Throwable; it is laziness.

    I would also argue that checked exceptions are no more complex than Eithers, Try or Applicatives. Actually passing Eithers or Applicatives around everywhere can easily clutter your code as well, IMO it can be worse than checked Exceptions.

    • lmm 2 years ago

      > And I guess this is precisely why so many developers hate them; they don't like the extra work they have to do to catch all those edge conditions. This is why we see so many empty catch blocks or upcasting to Exception or even Throwable; it is laziness.

      Good developers should be lazy. Checked exceptions require you to do a bunch of cumbersome ceremony that makes your code unreadable, for what should be (and is, with Either or equivalent) at most a couple of symbols; no wonder developers hate that.

      > I would also argue that checked exceptions are no more complex than Eithers, Try or Applicatives.

      You'd be wrong. Checked exceptions as implemented in Java require two keywords of their own (throw and catch), a special change to method signatures (throws), a unique new kind of expression for multi-catch (|), a unique new kind of type for things caught in multi-catches, and special support in every other new language feature (e.g. Futures have a whole bunch of extra code to deal with exceptions). Eithers can literally be a regular class that you could write yourself using ordinary language features. A small piece of syntax sugar (something like "do notation", "for/yield comprehensions", or "?") is a good idea, but not essential, and if you do it right then you can implement that once across your whole language (for applicatives/monads in general) and use it for eithers, futures, database transactions, audit logs, almost anything. https://philipnilsson.github.io/Badness10k/escaping-hell-wit... .

      • watwut 2 years ago

        Sorry but Either makes the code harder to read then exceptions. And harder to debug.

        • lmm 2 years ago

          Only if you're ignoring errors. If you actually catch and handle them the exception-based code becomes harder to read.

    • hedora 2 years ago

      Checked (and unchecked) exceptions create an exponential number of control flow paths. Consider:

         try {
            throws_a();
            r = grab_resource();
            throws_b();
            r.throws_a();
         } catch (a) {
            r.release(); // oops; null pointer some times
         } catch (b) {
            try {
              r.release();
            } catch (a) {
              // nooo....
            } 
         } finally {
           if r != null {
              r.finalize() // use after release (sometimes)
              r.release() // ???
           } 
         }
      
      There is plenty of academic literature showing that real programs are even worse than my contrived example, on average.
      • Supermancho 2 years ago

        The construction of arbitrary convolutions of logic is not demonstrative of your assertions. A multitude of code paths is not specific to checked exceptions. Note: It is possible to try catch a union of exception types in the recent versions of Java (catch X | Y | Z).

        The paths are still there if the exceptions are all unchecked or combined or not using exceptions at all. Passing exceptions up is rarely the right choice, imo. If it's a terminal runtime, sure, obv.

      • josefx 2 years ago

        > r.release(); // oops; null pointer some times

        Quite sure Java would prevent that if r was never assigned.

        Also most languages provide cleaner try-with-resources or RAII style lifetime management so you don't actually end up with that kind of spaghetti code unless you actively go out of your way to be bad at programming.

        • masklinn 2 years ago

          > Also most languages provide cleaner try-with-resources or RAII style lifetime management so you don't actually end up with that kind of spaghetti code unless you actively go out of your way to be bad at programming.

          Context managers (try with resources) specifically don't work at all when release is conditional e.g. open a file, need some post processing, need to close the file if the post-processing fails but return it if it succeeds.

          Defers don't either, unless you have a specialised one (e.g. zig's errdefer).

          Actual RAII (aka affine types) does work, but adding that to a GC'd langage after the fact is a huge undertaking and increase in complexity, because the interaction between affine and normal types is fraught.

    • renewiltord 2 years ago

      Ultimately, core language features that make things ergonomic enough for lazy developers to get it right will result in better software.

      And ones that are so unergonomic that only industrious developers will get them right will result in worse software.

      Modern languages allow these to be zero cost abstractions so there is little tradeoff.

      • misja111 2 years ago

        I have been coding in Scala/ Haskell for the last 10 years, before that 15 years in Java. What I'm seeing is that there are as many lazy developers in FP as there are in Java, maybe even more. And despite all the nice safeguards that FP provides, there are still plenty of ways for lazy devs to work around them.

        For instance the IO monad, which is used everywhere Scala/Cats. It can contain a result or an error. If you don't feel like checking for an error after you called some method, you can just pass it up and return the IO from your method. Does that sound familiar? It behaves just like a checked exception, the only difference is that methods don't need to declare any error or exception in their IO signature.

        • mrkeen 2 years ago

          > It behaves just like a checked exception, the only difference is that methods don't need to declare any error or exception in their IO signature.

          Then it behaves like an unchecked exception. Which is fine.

          'Checking' is the act of modifying your source code to declare the error.

    • skissane 2 years ago

      > And I guess this is precisely why so many developers hate them; they don't like the extra work they have to do to catch all those edge conditions

      I hate checked exceptions when they force me to handle an exception which I know is impossible given the arguments passed to the method. For example, some older Java APIs take an encoding name and force you to handle the checked UnsupportedEncodingException - even when the encoding name was something like “US-ASCII” or “UTF-8” which is required to exist by the Java standard, and if somehow they didn’t there is often no possible error recovery than just crashing. This has been fixed in newer versions by introducing new APIs which throw an unchecked exception instead, and also by introducing constant objects for the standard charsets

      • Supermancho 2 years ago

        > I hate checked exceptions when they force me to handle an exception which I know is impossible given the arguments passed to the method.

        If I want to ignore a set of exceptions, I have the option to catch(Exception e) {} signaling that I recognize the risks that have been explicitly communicated by the API (that throws). An @IgnoreExceptions annotation would help dump the 5? boilerplate lines.

        The unknown risks for other non-specific Runtime exceptions, are not included. I can catch those too if I add a catch(RuntimeException e){}, again signalizing that I recognize the risks such that other developers understand that I'm opting out of handling those conditions, which may or may not be errors in a classic sense. eg an expected socket not being available causing an IOException, because I'm doing some concurrent process.

        • skissane 2 years ago

          I've seen checked exceptions cause numerous bugs. Why? Because they encourage you to put catch clauses everywhere, and a lot of developers don't know how to write them properly (and even the best developers sometimes make mistakes that slip through the cracks). A common result of this is that a bug causes an exception, but the catch clause loses information about the original exception (such as by logging it without the stack trace, or throwing a new exception without setting the cause). Or else the catch clause just ignores the exception, and then you get some other error later (e.g. NPE because some field was unexpectedly null), but again you've lost the info on what the root cause problem is. If your code base contains 100s of catch clauses, that's 100s of opportunities for buggy catch clauses to exist. And even if you eradicate them all from your own code base, then you might get hit by one buried in a third party library.

          Without checked exceptions, you end up with far fewer catch clauses, and hence far fewer opportunities for buggy catch clauses. I think in most apps, most of the time, you just want to bubble all exceptions up to a central point which handles them – for example, in a web server, you can have a single catch clause for the whole HTTP request. If need be, you can make that central point extensible with custom handling with specific exceptions (like JAX-RS ExceptionMapper, or @ExceptionHandler beans in Spring – which you can then inject using IoC/DI). Once you've got that working, if you need catch clauses deeper in the code (e.g to return an error code to the frontend when the client sends bad data instead of just failing the request with a `NumberFormatException`), you can add them where appropriate – but unlike checked exceptions, you aren't forced to add them where you don't need them.

          • Supermancho 2 years ago

            > I've seen checked exceptions cause numerous bugs.

            I've seen quite a few in for-loops, so I'm not sure that's tracking for me.

            > Why? Because they encourage you to put catch clauses everywhere

            Java forces you to handle author-specified error conditions (CS error), but jr developers do tend to create their own more often than necessary.

            While I agree that a global exception handler is good practice for Spring applications, individual exception handling is very common and important to modern software. eg If Web APIs get more complex (multiple datastores), you find you don't want to bubble everything up. I get a request, want to check a cache (which might throw) then look it up in a DB (which might throw) then look it up in a file (etc), then return a response.

            I do wish I could handle exceptions simpler than making the choice to add 4 lines (+ any actual logging, etc) or blackhole-traveling through the stack to add checked exception signatures everywhere (code AND tests).

            • skissane 2 years ago

              > I've seen quite a few in for-loops, so I'm not sure that's tracking for me.

              The difference is that for-loops are almost an essential language feature – the vast majority of languages have them (or a close equivalent), and they make certain algorithms a lot easier to state clearly. Sure, there are some languages which lack them, but they tend to be either languages with non-mainstream paradigms (such as pure functional or logic programming languages) which put all the emphasis on recursion instead, or else really old legacy languages which predate the development of structured programming (such as pre-1980s versions of COBOL–modern COBOL versions have for loops)

              By contrast, almost nobody considers checked exceptions an "essential language feature" – languages which lack them vastly outnumber languages which possess them, indeed, Java stands out as the only major mainstream language to possess them

              Given the argument "this unusual inessential language feature causes more bugs than it prevents", the response "this essential language feature which the vast majority of languages have sometimes causes bugs too" isn't very convincing

    • bedobi 2 years ago

      > Checked exceptions however are as safe

      No. If I add a checked UserNotFound exception to a getUser db call, you can bet someone higher up the stack will do try catch Exception e, so now they're catching OutOfMemory and who knows what else.

      • twelve40 2 years ago

        > you can bet someone higher up the stack will do try catch Exception e

        But that's laziness on the caller's part. If I offer a method but the caller decides to do reckless lazy crap with it, there are many different ways to get there in any language. I typically call those out (Exception e) at code reviews.

        • bedobi 2 years ago

          This is the most common reply I see whenever anyone proposes a safer, better way of coding, and it's not a good one

          • esafak 2 years ago

            ...because doing the right thing should be convenient, for its own good.

      • kaba0 2 years ago

        As opposed to force unwrapping a Result type? Also, OutOfMemory is an error, exceptions won’t catch it.

        • unscaled 2 years ago

          Force-unwrapping a Result type is something you can do in multi-paradigm languages such as Rust, but not in stricter functional languages like Haskell - at least not easily (and we're worried about developers taking the easy way out here).

          But more importantly, force-unwrapping is not equivalent to catching generic exceptions. Instead, it's equivalent to catching all checked exceptions and wrapping them in a Runtime error. It's also almost equivalent to what this compiler plugin does (or Kotlin or Lombok's @SneakyThrows do).

          Catching "Exception" and trying to handle it generically, is more closely equivalent to this type of code:

            match result {
              Ok(value) => doSomethingWithValue(value)
              Err(e) => println("Error: {e}!")
            }
        • DarkNova6 2 years ago

          What about DivisionByZero, NumberFormatexception, ArrayStoreException?

          There are countless examples of rare but legit non-obscure use-cases. And even if your code is fine, you can't expect the same for the libraries you are using.

          And some exceptions make frequent non-happy paths more visible. Most of all IOException, because IO can _always_ fail for all the wrong reasons (because the failing of this exception is outside of the JVM's influence, it is rightfully a checked exception). And often you simply don't want to do the error handling at call-site but propagate to the code which is controlling the use-case.

        • clhodapp 2 years ago

          You can choose to unwrap the result type wherever you want, as opposed to it automatically unwrapping in place at the call site and having a weird propensity to encourage a "special" return from there.

      • vips7L 2 years ago

        OutOfMemoryError isn't an exception so they would not be catching it.

        • bedobi 2 years ago

          whatever, the point is exception allows and encourages people to catch less and more than they should

          • vips7L 2 years ago

            Sometimes top level code needs to do this. It's not always wrong. It's not always black and white in programming.

            • bedobi 2 years ago

              > Sometimes top level code needs to do this

              come on, it's not as if anyone disagrees with that, but that's extremely, extremely rare, overwhelmingly, callers are just dealing things like with UserNotFoundException, NullPointerException, what have you, and there's no reason why the compiler should happily let you catch Exception (or nothing) when it could just be giving you an honest object back

              • wewtyflakes 2 years ago

                It has not been extremely/overwhelmingly rare in my experience. Further still, within the context of Java, exceptions are objects.

      • jbverschoor 2 years ago

        Then that person should get a Java 101 class. Or is this the current “staff software engineer” level of skills?

        • bedobi 2 years ago

          This is the most common reply I see whenever anyone proposes a safer, better way of coding, and it's not a good one. "Just get better" like oh ok except that in the real world people are gonna people and even the best programmers in the world make mistakes and do dumb, lazy shit. Our tools should be designed such that the safest, most correct way to do anything has the path of least the resistance. Not happily allow you to ignore exceptions, or catch less or more exceptions than required. Functional error handling corrects this, exceptions do not. Anyway, it's clear we're not going to agree, and you win, since so far the industry is still stubbornly clinging to exceptions, despite them being a failed feature in every language they're in.

          • jbverschoor 2 years ago

            It’s not even lazy, your IDE will not do this for you by default.

            It’s got nothing to do with getting better. It IS basic Java exception handling. Any proper course or tutorial will tell you to catch specific exceptions

            • bedobi 2 years ago

              > Any proper course or tutorial will tell you to catch specific exceptions

              any real world library or application will catch Exception e, catch checked exceptions and turn them into unchecked ones etc etc

    • nradov 2 years ago

      I don't hate checked exceptions because they make me do extra work, I hate them because they inhibit the proper use of inheritance for OO design.

  • The_Colonel 2 years ago

    > Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more.

    Which is bad. In 99% of cases I have no specific error handling for the given problem. Just let the process crash or be handled by application server / framework.

    Representing these kinds of error conditions in the code which I have no interest in handling is just noise.

    • bedobi 2 years ago

      The problem with that is you lose all ability to understand or control what a given endpoint will return in any given situation

      > I have no specific error handling for the given problem

      then pass it on and decide what to do with it at the system boundary

      in the db:

      fun getUser(uuid: UUID) : Either<Error, User>

      middle layers: pass around the either, you can map, flatmap etc on it to chain it with other computations there

      then in the resource layer

      return user .fold({ error -> when(error) { is DbError -> HttpResponse.InternalServerError() is UserNotFoundError -> HttpResponse.NotFound } }, { user -> HttpResponse.ok(user) })

      Then, at every layer, each method explicitly says in the method signature what it returns. There’s no need to look around each and every line of every method in every layer, or in the framework, or some global exception handler, to figure out what will happen. Developers can tell from a glance at the resource method what the endpoint will return for each outcome, in context.

      Things like being unable to communicate with the DB or not finding a user are not exceptional, they're entirely expectable when you're calling a db to look for a user and should be modeled accordingly.

      • The_Colonel 2 years ago

        > The problem with that is you lose all ability to understand or control what a given endpoint will return in any given situation

        Why? In the simplest (but pretty common) case it's:

        - successful response

        - generic error message

        > then pass it on and decide what to do with it at the system boundary

        Yes, but if in 99% of cases I only pass it on, it's just visual noise. Noise you become blind to, and it loses meaning.

        > Developers can tell from a glance at the resource method what the endpoint will return for each outcome, in context.

        They can't really because you end up with some very generic error type anyway. Any typical service can have IOError, DBConnectionError, SQLError, OutOfMemoryError, StackOverflowError plus many other application specific errors. You end up with bulk of your methods returning Either<Error, Something> which is then meaningless.

        • WirelessGigabit 2 years ago

          It feels like you're trying to use Exceptions as a way to steer your logic, otherwise why would you need to know why an operation failed to such detail?

          Your controller method cannot act differently on a DBConnectionerror or OutOfMemory error.

          Not to mention that exceptions cause developers to use them as control flow mechanisms.

          For example searching a user by id. If the database returns 0 records, is that reason to throw an exception? Well, doing result[0] results in IndexOutOfBounds due to result being [].

          But the reality is that the user not being there isn't exceptional. Typos are common. By using Result<T, E> or Either you enforce the developers to think more about their flow. One can write the method like this:

             fn find_by_id(id: usize) ->Result<UserResult, Error> -> {
                 let raw_db_result = db.search("SELECT id, first_name, last_name FROM user WHERE id = ?", id)?;
          
                 match raw_db_result {
                     None => Ok(UserResult::NotFound),
                     Some(r) => {
                         let only_user = r.ensureOneResult()?;
                         let user = mapDBUserToUser(only_user);
          
                         Ok(UserResult::User(user))
                     }
                 }
             }
          
          What about Error? Reality is that I don't really care. Error doesn't contain anything that is actionable. Either the whole chain succeeds and returns a valid result or the Error. The caller wants to find a user by id. There is one, or there isn't. All the rest is just errors that they'll pass on too. And in the end they get logged and result into Error 500.

          A 404 is actually a valid result.

          Now, if I were to use a throw new UserNotFoundException() for no user found you end up with generic try catches catching too much. And now someone needs to go and take it all apart to identify that single Exception that they want to deal with separately.

          Whereas if I want to add a state in my enum the callers _MUST_ update their code due to how Rust works.

          • The_Colonel 2 years ago

            > otherwise why would you need to know why an operation failed to such detail?

            I'm not defining the errors like DBConnectionError or OutOfMemoryError - it's the framework/platform which defines them and throws/returns them.

            > But the reality is that the user not being there isn't exceptional.

            That depends. In some contexts it is not exceptional (getting user by ID given as an argument to webservice), in that case using Maybe type is great. In other contexts it is very much exceptional (e.g. signed JWT refers to a non-existing user) and throwing Exception makes more sense.

            > What about Error? Reality is that I don't really care. Error doesn't contain anything that is actionable. Either the whole chain succeeds and returns a valid result or the Error. The caller wants to find a user by id. There is one, or there isn't. All the rest is just errors that they'll pass on too. And in the end they get logged and result into Error 500.

            Which is the same as exception. But now you have this visual noise of something you claim you don't care about. An unchecked exception makes this irrelevant noise go away.

            > Now, if I were to use a throw new UserNotFoundException() for no user found you end up with generic try catches catching too much. And now someone needs to go and take it all apart to identify that single Exception that they want to deal with separately.

                try {
                    ...
                }
                catch (UserNotFoundException e) {
                    // handle ...
                }
            
            I'm catching exactly the exception I want. Where I'm catching too much? Where do I need to take it apart?

            (This particular example of exception usage is bad, though, as it smells of exception control flow. Here using Maybe type would be better)

            > Whereas if I want to add a state in my enum the callers _MUST_ update their code due to how Rust works.

            Which is good for some cases where the enum describes "business" cases.

            But it is pretty bad for truly exceptional cases (which are unlikely to be handled anyway). Library adding a new exceptional case will break its clients, which seems like a bad trade-off (again, for truly exceptional cases).

            • anon-3988 2 years ago

              ```

              try {

                      ...
              
                  }
              
                  catch (UserNotFoundException e) {
              
                      // handle ...
              
                  }
              
              ```

              This is just bikeshedding, because the equivalent code in Rust would be. For example, if there's multiple lines in the try block, who exactly returned this error? Are there other errors I didn't handle? Are there unexpected error that I forgot to check. For example, the "get" function of arrays in many languages usually always return T. But this is actually a lie because the array might be empty. So by right, it should return Option<T>. But exception based programming have basically created this illusion that its infallible. How many people check their array accesses?

              ```

              match value {

              Ok(ok)=> {...}

              Err(UserNotFoundException(e)) = { handle }

              e => return e

              }

              ```

              Which does look more complicated, but it scales way way better when you have multiple errors

              > But it is pretty bad for truly exceptional cases (which are unlikely to be handled anyway).

              But why should a language be designed for exceptional cases? Errors are not exceptional at all. In the above code, the actual code will actually look like this

              ```

              let rows = db.get_rows(query)?; // returns Result<Vec<DbRow>, E1>

              let first_row = rows.first()?; // returns Option<DbRow>

              let user = first_row.to_user()?; // returns Result<User, E2>

              return user

              ```

              Exception-based language will have the same looking code, but then imagine what would happen if you try to figure out which functions return what. You have no other recourse other than to dig into the source code to find all the unchecked exceptions that it can throw.

              Another example, how would an exception language write this code to get multiple rows from the db and map each row to its respective user.

              ```

              // get_rows and to_user both can fail

              let users :Vec<User> = db.get_rows(query)?.map(|r|r.to_user()).collect::<Result<_,_>()?;

              ```

          • arcbyte 2 years ago

            Actually, I have some java code that handles OutOfMemoryError when I do image resizing. Of the image is so big that the decompressed versión can't fit in half a gig of memory (which happens) then I fall back to some crude subsampling to size it down. It's unnoticeably less pretty but works is how o handle OutOfMemoryErrors in a specific scenario.

          • smallerfish 2 years ago

            Or Kotlin's approach:

               fun getUser(userId: UUID): User?
            
            You cannot treat this result value as a `User` in code that calls this; though, once you null check it in your service layer, you can pass it on as `User` in the not-null branch. Null is nothing to be afraid of if the language forces you to declare nullability and deal with it.
            • andrekandre 2 years ago

              this is a very common in obj-c too

              on the other hand, depending on what your trying to do you might want to provide more context about what happened to the user/programmer

              in swift you can change a throwing function to a nullable with `try?` so even if `getUser()` throws, you can keep it simple if thats what is appropriate

                guard let user = try? getUser(id: someUUID) else {
                    return "user not found"
                }
              
              as an aside, swift "throws" exceptions but these are just sugar for returning basically an Result<T,E> and compose much better than traditional stack unwinding exceptions imo
          • bedobi 2 years ago

            WirelessGigabit gets it. That last fact is huge - functional error handling forces callers to handle (or pass on) exactly what can go wrong, no more, no less. (unlike exceptions)

            • wtetzner 2 years ago

              That's not really true in practice. Often you just end up with a massive Error variant type that's used everywhere, even when the specific function you're calling could only return one of them.

              • WirelessGigabit 2 years ago

                You don't have to have Error variants if you don't care about their type.

                You cannot catch a StackOverflowException or an OutOfMemoryException. All you do is log it & restart the app.

                Once could split it into retryable errors and non-retryable ones. Like a DB disconnect, that can be retried.

                BUT to be fair, that's not logic that belongs in the business.

        • bedobi 2 years ago

          You're describing the problems with exception, not using Either... When using Either, you never fold on those errors individually... Anyway, it's clear we're not going to agree, and you win, since so far the industry is still stubbornly clinging to exceptions, despite them being a failed feature in every language they're in.

    • titzer 2 years ago

      Situations vary a lot. In general there's no guarantee that a far outer scope is going to know how to deal with errors from deep inside some nested calls except for some very general logic, like failing a whole operation.

      • kaba0 2 years ago

        Even more commonly, there is no meaningful error handling to be done at a layer above the call.

        • bedobi 2 years ago

          I don't mean to be uncharitable but it seems like most exception advocates here don't really understand the case us functional error handling advocates are making. We don't advocate handling errors at every level. They're usually sent to the boundary layers of the system, eg the http resource layer, and folded there into http responses.

    • anon-3988 2 years ago

      In Rust you can just do "let value = result.unwrap()".

    • sam_lowry_ 2 years ago

      Especially because Kubernetes and the like work as if the application must die anyway. Pretty much like PHP CGI invocations always worked since 1998 or so.

  • spion 2 years ago

    Exceptions are not unsafe.

    That said, not modelling them in the type system is a mistake. But the model has to be useful - knowing what functions can or cannot throw is useful, knowing what they throw, less so.

    (They are safe because of try-finally / try-with-resources)

    • bedobi 2 years ago

      They are unsafe because they invariably result in people either ignoring them or catching more than they should, or less than they should, and the compiler happily lets you do that, EVEN when you're using checked exceptions.

      • AnimalMuppet 2 years ago

        How will the compiler let you catch less than you should when using checked exceptions?

      • spion 2 years ago

        There is no should/shouldn't. If you don't have a specific error in mind and how to handle it, you shouldn't handle it. In practice most errors can be left unhandled all the way to e.g. server response, so this works quite fine. `try-with-resources` is typically awkwardly implemented unfortunately (defer is nicer, the new `using` keyword in JS is quite nice too)

        • bedobi 2 years ago

          I don't mean to be uncharitable but it seems like most exception advocates here don't really understand the case us functional error handling advocates are making. We don't advocate handling errors at every level. They're usually sent to the boundary layers of the system, eg the http resource layer, and folded there into http responses. The safety case we're making has nothing to do with try catch finally, it is about signatures and what callers are forced and allowed to do.

  • wvenable 2 years ago

    > Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more.

    That's what wrong with them. Callers, in almost all cases, do not know what to do exceptional outcomes. So you force programmers into a situation where they're dealing with things they shouldn't be dealing with.

  • asddubs 2 years ago

    You mean something like C++ Optional, right? Those are nice, if the language supports generics.

asddubs 2 years ago

I actually quite like checked exceptions, and miss them in other languages. My biggest gripe with exceptions is that a few calls deep, you can no longer tell whether calling something might throw or not, and what type of exception it might potentially throw.

  • _old_dude_ 2 years ago

    Checked exceptions are a leaky concept in Java. The Java type system has no union types so you can not have a generics method that abstract more than one exception.

    That's why Stream::map can not capture the checked exceptions properly.

    • rozap 2 years ago

      Definitely they have shortcomings in Java but throwing the concept out entirely is a bummer. It creates a whole shadow type system where all bets are off.

    • pharmakom 2 years ago

      Yes! The problem with the Java implementation of checked exceptions is the inheritance hierarchy, which encourages catching the broadest possible error.

  • josephcsible 2 years ago

    I like checked exceptions in principle too, but Java's implementation of them is so bad that I hate them there. The first major mistake is that it doesn't support exception polymorphism (e.g., I wish you could pass a comparator to Arrays.sort that throws checked exceptions, and have that call to Arrays.sort itself then throw the same checked exceptions), and the second is that the standard library makes a bunch of exceptions checked that should be unchecked (e.g., IOException from close()).

nepthar 2 years ago

Reading through the comments, I realize this may be a minority view - but I like coding for the happy path and letting exceptional states crash. I find languages like go a bit harder to parse quickly because I always have to "unwrap" the happy path from all of the mixed in error handling.

I'm sure I'd get used to it eventually, but I like that unchecked exceptions in Java are now an option!

  • Delk 2 years ago

    It really depends on what you're developing.

    If it's some random web backend, it's often fine to just let the error propagate as a 5xx. Many error cases wouldn't have a better solution anyway, and all that's breaking is a single page or resource load. (Of course on the frontend it might be nicer to show some kind of an error than just e.g. having frontend functionality silently fail.)

    If it's a desktop application and you're crashing the entire application because wifi happened to drop or whenever there's anything your happy path didn't predict, that's going to be bad.

    If it's some kind of an embedded thing and you're crashing an entire device because you didn't bother to handle cases that are outside of the happy path but entirely possible, I only hope it's not something important you're developing.

  • asddubs 2 years ago

    I'm fine with doing this on purpose, but without a system like checked exceptions, you do it without even really realizing you're doing it. Checked exceptions point out the errors and then let you decide whether it's something you should handle or let it crash. It makes for more stable software.

    • wvenable 2 years ago

      The number of exceptional errors one can reasonably handle is so small to be insignificant. Aborting the current operation (which could be just one task or the whole application) is the most common result. Abort and log. Unchecked exceptions make that easy. Checked exceptions make that long and tedious and add nothing.

      What and where you can handle exceptions has nothing to do with where the exception is thrown. Handlers only exist at key points in the application at the start of operations where you can skip, abort, or retry.

    • code_runner 2 years ago

      its only an illusion of stability. so many things can go wrong outside of exceptions and all it does is add mandatory lines of code (probably rethrowing as a runtime exception) to every single consumer. It pollutes everything it touches.

    • bottlepalm 2 years ago

      I didn't know Java apps were so much more stable than C# ones.

  • ziml77 2 years ago

    If you actually let it crash that's fine, but a lot of people just toss a catch (Exception ex) in there because they don't know what exceptions will happen and don't want errors that aren't actually a big deal to bring the system down. But the issue with catching everything, even if you log before continuing, is that the process may be in a bad state and you're just continuing with it like that. I got to see a couple database tables get absolutely mangled by a process that had an exception that was caught and logged. That was a pain to clean up since doing nothing more than restoring a backup would have lost the entire day's worth of business operations in that system.

  • EspressoGPT 2 years ago

    This is what I like about Go and other more recent languages: Especially when networking or other IO is involved, things will go wrong. These languages don't try to circumvent this reality but instead embrace it by treating errors as very-first-class citizens. My error handling tends to be more thoughtful in those languages than in, say, Java.

  • the_gipsy 2 years ago

    In practice there is a lot of paths that you don't want to "crash" on, but try the next thing etc.

    • mrkeen 2 years ago

      Don't model those as Exceptions, because they are not exceptional. They are control flow.

      • the_gipsy 2 years ago

        You can't have your pie and eat it too. Some callers will want a Result, some will want to panic. A Result can be panicked on very easily, but handling control flow with exceptions and try/catch has been proven a huge failure.

      • randyrand 2 years ago

        That idea just leads to confusion, as the line between exceptional or not will always be blurry, and depend on how it is called, the specific input data, your hardware state, etc.

        you basically end up needing to write your api twice.

  • raincole 2 years ago

    > I like coding for the happy path and letting exceptional states crash.

    Of course everyone like coding like this. No one likes to code error handling code.

    Just like no one likes to code tests and documentations.

    It turns out not every important thing is joyful.

clownvorld 2 years ago

The Trouble with Checked Exceptions A Conversation with Anders Hejlsberg, Part II

https://www.artima.com/articles/the-trouble-with-checked-exc...

  • taftster 2 years ago

    This is an insightful interview, thank you for the link. I'm well read up on the topic, but this interview was still great and is a good perspective on the checked/unchecked debate.

    The fact that Java has introduced UncheckedIOException, in my opinion, shows how some people in the Java community have come to believe that checked exceptions were a mistake (understanding that lambda forced the issue). There's probably not too much to be easily done at this point, but consideration for changing checked exceptions in the JDK to extend RuntimeException sure would be interesting.

zeedude 2 years ago

Manifold already provides this feature:

https://github.com/manifold-systems/manifold/tree/master/man...

With an article having the same name as your post:

https://github.com/manifold-systems/manifold/blob/master/doc...

nnnnico 2 years ago

Allowing unchecked exceptions in languages without explicit error handling or the return of error values is a mistake IMO! Makes it impossible to call a function safely

  • RGBCube 2 years ago

    Exactly, C++ exceptions are horrible, you never know what throws what. Java made C++ exceptions better by making them explicit, so you always know what throws. Then Kotlin came and made everything a unusable mess (don't get me wrong, I love Kotlin, just hate that it doesn't have explicit exceptions).

    I honestly think the best error handling strategy is employed by Zig, then Rust. they're very explicit while not getting in the way, you always know what throws what and what doesn't.

    Rust has a little issue though, which is that people can make their functions return `Result<T, Box<dyn Error>>` which makes handling the returned error difficult, Zig does not have any of that.

    • ziml77 2 years ago

      As far as I know Zig errors can't carry any data along. It's nice having more details available about the failure.

      Take for example if you call to an OS API and it returns an error that wasn't documented to be returned from that function. You can't return that code up the callstack using Zig's error mechanism. Instead there's functions such as unexpectedErrno and unexpectedError. Those call the appropriate method to get a string representation of the error and call std.debug.print to display it, then they just return error.Unexpected.

      That means that the caller doesn't have any control over how the error is displayed. Meanwhile in Rust, you can add an UnexpectedError variant to your enum and let it carry an error code with it. The caller can then display that error however they want.

      I don't hate Zig's error handling by any means, but personally I think Rust does it better. I'm happy to see that C++ seems to be going with the Rust way by adding a Result-like type std::expected.

      • RGBCube 2 years ago

        Both have their issues, but I think the perfect error handling mechanism would be something like the Zig way with additional data.

    • menaerus 2 years ago

      It is true that C++ exceptions are not checked and are rarely part of the interface signature but C++ exceptions are not a tool for general error handling in the context you're comparing them against Java, Zig and Rust. Java uses exceptions for control flow while C++ doesn't. Zig and Rust don't have exceptions at all. What Zig and Rust have as error handling mechanisms C++ has them too. C++ exceptions are of a literal meaning - exceptional cases which you don't expect to happen and you usually don't know how to recover from. In this case propagating the exception up to the thread entry point and doing whatever in that case (logging, terminating, restarting, etc.) is vastly better than language forcing me to check for each possible exception all the way through the whole function callstack - it's useless.

      • masklinn 2 years ago

        > It is true that C++ exceptions are not checked and are rarely part of the interface signature but C++ exceptions are not a tool for general error handling

        They’re literally the only way to report ctor errors.

        > What Zig and Rust have as error handling mechanisms C++ has them too.

        Only in the most reductive way that all langages are turing complete, in which case Java has them as well.

        • menaerus 2 years ago

          > They’re literally the only way to report ctor errors.

          The point being? Please go find me a codebase which wraps object instantiation in try-catch clause if that's what you are trying to say. I haven't seen any.

          Anyway, for that reason constructors are always written so that they cannot fail in hideous ways, and if there is no other way around it there is always 2-phase initialization (personally I think it's an anti-pattern).

          > Only in the most reductive way that all langages are turing complete, in which case Java has them as well.

          Nonsense. Zig AFAIU abstracts error types as some sort of integer enums while OTOH Rust has Result<T, E>. C++ has both of those mechanisms baked either into the language (enum) or standard library (std::expected, std::optional).

      • AnimalMuppet 2 years ago

        In what way are Java exceptions part of flow control and C++ exceptions aren't?

        • menaerus 2 years ago

          Although theoretically possible there will be rarely any C++ codebase that will use exceptions for control flow.

        • vips7L 2 years ago

          Honestly who cares if they are? Error handling is control flow.

    • SomeRndName11 2 years ago

      I think the way Go does error handling is the best compromise.

      • kaba0 2 years ago

        Go’s solution is like people couldn’t decide on if they should turn left, or turn right to avoid the cliff, so to make everyone happy they drove straight into it.

        It is literally the compiler enforced shitty error handling from C’s errno that is not even a sum type.

      • ziml77 2 years ago

        It's one of the worst I've seen. It adds so much cognitive overhead to reading the code and makes it more annoying to write. Every function call that can fail is followed by 3 lines to check for and return the error. The actual logic quickly gets lost in there.

        If it wasn't for the error handling, I would probably use Go for some of my projects. It compiles to a native, self-contained binary and still has the convenience of a garbage collector, and of course has plenty of libs available due to popularity.

        • SomeRndName11 2 years ago

          It does not have all that undpredictability of exceptions, and does not have nasty error handling logic of C. Do you know better alternative?

      • vips7L 2 years ago

        It’s honestly the worst compromise. You can trivially ignore any error in Go and they don’t contain contextual information about the call stack.

        • SomeRndName11 2 years ago

          ABility to ignore errors is a plus in fact. You can skip trivial non-critical problems and carry on without crashing the whole app, like it is normally done in C and C++.

          • vips7L 2 years ago

            It would be if you would have to explicitly opt into it via an ignored variable like _ or something. But currently with go you can accidentally ignore the error when you don't mean to.

      • the_gipsy 2 years ago

        It's a successful compromise, but probably not the best. It has a huge amount of little issues that simply aren't there with FP error handling.

  • _old_dude_ 2 years ago

    In Java, no method call is safe (Haskell/Rust safe), the langage will hapilly throws a null pointer exception or an out of memory error.

    But I don't think C#, Kotlin or Scala are less safe than Java even if they do not have the concept of checked exceptions.

  • tasubotadas 2 years ago

    The entire community disagrees with you[1]

    >Makes it impossible to call a function safely ?? catch(Exception e) anyone?

    [1] https://literatejava.com/exceptions/checked-exceptions-javas...

  • rho4 2 years ago

    I would argue the opposite: Junior and senior developers alike catch and swallow or log checked exceptions all over the place, making it impossible to know if your method call was successful or not.

    • misja111 2 years ago

      This is why you should have code reviews. Letting juniors submit code without reviewing it is a recipe for disaster, no matter which language you are programming in. And if it's a senior, well someone should have a word with him ..

    • Supermancho 2 years ago

      At some point you want to swallow all checked exceptions at some point, because letting the application leak stack traces to the client is generally a bad idea.

  • wvenable 2 years ago

    What does it mean to call a function safely?

    I have a desktop application with a single exception handler wrapping it's event loop. The handler display the message to the user and returns the to loop. It's rock solid -- nothing can crash it. The user can try to save their file to some network share that just disappeared and when that fails just save again somewhere else.

    But there is no such thing a safe function to call. Whether you handle errors by meticulously passing them around everywhere or not makes little difference.

  • ars 2 years ago

    Java just needs one small change to fix exceptions.

    Instead of the exception type being checked or unchecked, the throw should specify checked or unchecked.

    So for example the first time the exception happens it can throw a checked exception for the caller to deal with.

    If the caller doesn't deal with it it can simply throw its way all the way to the top without every single function needing to declare they handle it.

    • kaba0 2 years ago

      The proper solution is effectful type systems that are polymorphic to effects, like exceptions.

  • nnnnico 2 years ago

    (To clarify, i'm against exception based error handling, but removing checked exceptions in languages without other mechanisms of explicit error handling makes things more brittle. For example in kotlin, where some Java practices to throw all over the place are still in use, unchecked exceptions makes things worse. You wont know if a function will throw unless you inspect the code or trust it's documentation)

w10-1 2 years ago

The friction between the benefits of detecting errors statically, handling them quickly, and the diversity of needs for error handling cannot be avoided with any fixed policy like "unchecked only".

This tool does remind us that checked exceptions in Java are a compile-time phenomenon only, so (non-compliant) compilers can be made to ignore them. Neat, but... helpful? It would certainly lock you into using the tool.

Instead, we need configurable policies for error handling, where the same code can be statically analyzed much more deeply (e.g., to include unchecked exceptions) or run more leniently (e.g., when exceptions are used only for flow control). Some of this might pertain to local declarations, and some might be policies of various scopes (per VM, per-package, or per call-chain), and would probably fold in assertions and fibers/virtual threads and some OS-sensitive behaviors. The goal would be to write the code once (with full disclosure, i.e., only check-able exceptions and assertions), but to reuse the same code under different policies.

Whether anyone wants to do the work of folding this into the VM as security privileges were is another question...

  • smrtinsert 2 years ago

    Agreed. I absolutely prefer checked exceptions since it forces devs and consequently business to develop requirements for possibilities. Lack of ambiguity helps me sleep at night. Potential runtime errors do not.

    • riku_iki 2 years ago

      The issue with checked exceptions in Java is that they are not supported in java lambda syntax and library functions.

      • masklinn 2 years ago

        More generally the problem of checked exceptions in Java is that they are not supported by Java.

        That was an issue right from the start, and it only got worse as the type system got richer.

        • riku_iki 2 years ago

          > More generally the problem of checked exceptions in Java is that they are not supported by Java.

          would you elaborate this statement?

theanonymousone 2 years ago

Putting aside the discussions about the necessity of checked exceptions (to which I feel unqualified to express my opinion), I believe the approach used by the OP article to remove them is not the best one:

- It requires modifying compiler arguments and putting some file in the local classpath! That's really not Java-ic. Is it?

- It is not transparent in the code. You cannot infer by looking at the source code that something has changed.

Since some time ago I use the NoException library[0] in my Java projects which achieves the same goal but without the above-mentioned issues. It can also be used to mimic Scala's Try construct.

[0] https://github.com/robertvazan/noexception

dtech 2 years ago

Most other languages agree that checked exceptions are not good by not having them.

As for alternatives, Try/Result and similar monads have decent adoption even in Java, but personally I quite like the Kotlin philosophy [1] to not have generic error containers and either use runtime exceptions for errors that cannot be reasonably handled by the caller and encapsulate failures in the return type if they can.

[1] https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/...

earthboundkid 2 years ago

Conceptually there’s not much difference between checked exceptions and multiple returns or returning a tuple with an error etc. The problem with Java is that the checked exceptions are too fine grained, so the signatures are brittle and non-composable. Everything should just throw an Exception and then use dynamic typing to figure the rest out, and then you wouldn’t have the problem that your toList method can’t compose with something that throws an unknown Exception subclass.

phoe-krk 2 years ago

> Unchecked does not make any changes to your bytecode. This is possible because the JVM does not know about checked exceptions. It's been the compiler holding you back all this time.

TIL!

galaxyLogic 2 years ago

This seems like a major improvement for Java readability as shown by the examples.

What I don't like is the examples of having to use Maven or Gradle to install it. Why does that have to be so verbose, and something written in another language?

  • rogerkeaysOP 2 years ago

    Ha, yeh. At first I had the command line examples first, but everyone was going on and on about maven and stuff, so I figured that's what the audience wants.

    [EDIT] I moved the command line stuff back to the top of the README. XML makes my eyes bleed too...

briffid 2 years ago

Checked exceptions turn out to be part of the API, meanwhile violating Liskov substitution principle and preventing some design patterns. This is because a superclass implementation might throw a checked exception, then its subclass not necessarily. However, if the caller refers the subclass instance via the superclass interface it must catch the exception, but if it refers via the subclass it must not catch, as that results in a compile error. So the point is that with checked exceptions you cannot replace an interface with any implementation, which should be possible in proper OO programming.

  • Hackbraten 2 years ago

    That’s not a Liskov violation. Changing the compile-time type of a local variable in the scope of the call site is not substituting the implementation of the referred-to object. Those are two different things.

    As you correctly mentioned, if you leave the compile-time type of a variable unchanged, then assign a different object (one that belongs to a subclass that doesn’t throw the exception when you call its method) to that variable, and then call that same method, the compiler still forces you to catch. That’s the Liskov substitution.

dejj 2 years ago

Lombok’s @SneakyThrows

https://projectlombok.org/features/SneakyThrows

throwaway2037 2 years ago

I never forget reading the javadoc for Google Gson class com.google.gson.JsonParseException (extends RuntimeException):

    This exception is a {@link RuntimeException} because it is exposed to the client. Using a {@link RuntimeException} avoids bad coding practices on the client side where they catch the exception and do nothing. It is often the case that you want to blow up if there is a parsing error (i.e. often clients do not know how to recover from a {@link JsonParseException}.
I have *completely* the opposite style: Throw checked for anything that can throw, except: null pointers and invalid arguments (e.g., negative port number). Why so much checked? The caller cannot hide from it. They are forced to ack it, else compiler error. At the very worst, they are free to rethrow as a wrapped RuntimeException (ugh).

When I write code in C#, I always left wondering: Does this method throw an exception (ignoring null pointer and invalid arguments)? It bothers me. And, I write that as someone who really likes C# -- it's a great language for my needs.

And, yes, I can appreciate that many people don't like the friction associated with checked exceptions. It does add a lot of visual bloat! Serious question: What are the alternatives that _communicate sufficiently_ through an interface? Honestly, I struggle to think of a better alternative, so I just live with it.

Just like the bad old days of integer return values used to indicate error state (C-like languages), you need to check every single call -- there are no shortcuts, especially with I/O. For me, it is similar with Java and C# when writing I/O code: You need to catch and correctly handle every (checked-ish) exception.

rho4 2 years ago

Still hoping that Java will support such a choice (compiler option) out of the box in the future.

Also, without IDE support such a plugin will never gain a lot of traction.

exabrial 2 years ago

I love the idea of this project. Just needs some IDE and possibly some verifier support!

I do just want to correct a tiny piece of the readme:

> a common practise is to rethrow it as a RuntimeException. The problem with this, ... is that the root cause of exceptions get hidden,

This is most definitely not true. If you wrap / re-throw as a RuntimeException you will absolutely get your original stack trace.

edpichler 2 years ago

This dependency removes one of the best features of Java, which make others think about what happens if things go wrong.

lucasyvas 2 years ago

This is a mistake - checked exceptions and functional equivalents are the clear path forward.

watwut 2 years ago

Nothing better then having to guess whether the method may throw an exception or not.

  • dtech 2 years ago

    You already have to guess. All method calls van throw Runtime exceptions.

  • code_runner 2 years ago

    nothing better than someone assuming a method can only throw a specific type of exception or won't throw at all.

vbezhenar 2 years ago

Checked exceptions are one of biggest mistakes Java ever made.

That said, patching compiler is not something I would do.

What I usually do is add unchecked exception class for every checked exception I had to "handle". So I have UncheckedIOException from standard library (handy!), UncheckedSQLException and so on and so on.

Yes, it causes more code, but Java is not exactly known for brevity, so be it. At least that way is not hacky in any way and actually used by Java standard library itself, so I'd consider it "idiomatic".

  • rogerkeaysOP 2 years ago

    You can also rethrow checked exceptions as runtime exceptions by exploiting type erasure:

        public static RuntimeException unchecked(Exception e) {
            Exceptions.<RuntimeException>throw_checked(e); 
            return null; 
        }
        private static <E extends Exception> void throw_checked(Exception e) throws E {
            throw (E) e;
        }
    
    then you can do this:

        try { ...
        } catch (IOException e) {
            throw unchecked(e);
        }
    
    And the original exception is thrown without being wrapped. It's as though you had `throws IOException` on your method signature.

    This is from my original solution to the problem: https://github.com/rogerkeays/jamaica-core/blob/0cc98b114998...

    • vbezhenar 2 years ago

      There's one big problem with this approach. You can't catch checked exception if the try-block does not throw it (according to compiler). That's compiler error.

      So yes, you can throw IOException masking it to RuntimeException, however you can't catch it later without another gimmicks like catching Exception and checking if it's IOException (and even that check causes linter warnings in Idea, so now you need to suppress those warnings...).

      Throwing and catching UncheckedIOException does not have this problem.

      • rogerkeaysOP 2 years ago

        Good point. Actually the plugin didn't handle this case either (fixed now). Thanks for the feedback.

flerchin 2 years ago

Neat. Reminds me of lombok in that it really only affects compile-time and makes for cleaner code in a way that many (but not all) developers would want.

  • jameslars 2 years ago

    Ugh Lombok! Literally everything it does is replaced by any competent IDE with auto-generated methods, with the added benefit of not requiring special build handling steps because the library can't play by the normal annotation processing rules.

    There was maybe a time Lombok made sense. It does not anymore. Death to Lombok.

    • nabogh 2 years ago

      Disagree. Just because the ide wrote a bunch of boilerplate for me at some point doesn't mean I can know the boilerplate is unchanged without reading a bunch of getters and setters. The mental burden of tiny classes is so much nicer to read.

      • jameslars 2 years ago

        This is every Lombok lover's favorite strawman argument I've run into.

        I've been coding in Java professionally for ~20 years. I can count with zero hands the number of times I've been burned by a getter or setter getting changed into something surprising.

        If you really need auto-generated getters/setters/builders - Immutables [1] is a library that does it using bog standard annotation processing rules that don't require hacking your build process.

        [1] https://github.com/immutables/immutables

    • never_inline 2 years ago

      That's usually one time concern. Much better than having boilerplate lying around IMO.

alanfranz 2 years ago

I don't want to remove all checked exceptions in random code, btw. Even though they're not common, it can be bad if they're unhandled in some cases. How can I know beforehand?

I'd like something to soften some of them, probably in a configurable way, in some builtin interfaces btw. E.g. many IOExceptions should really be unchecked.

javajosh 2 years ago

Like the idea, but I probably won't use it. Yes, checked exceptions are annoying, but the correct place to fix them is the source. This compiler plugin blinds you. The ordinary ways to mitigate checked exceptions (wrapping them in unchecked exceptions) is ugly but its also explicit.

Note: I will try it, because maybe I'm wrong.

  • rogerkeaysOP 2 years ago

    You still get warnings about checked exceptions from this plugin, so it's not entirely flying blind.

code_runner 2 years ago

To the credit of the die-hard Java community, you all really seem to love and support even the most horrific syntax and awful language features. You love the pain. The rest of us are here for you... I promise software can be fun.

Checked exceptions are far and away the worst part of java and I'm glad that no other language I've personally encountered have them!

On a more real note, I work at a shop with a fair amount of java now and checked exceptions are definitely my biggest complaint. I hope we adopt this, I'm sure we won't, but I'm glad to see a little movement on what I feel is the crusty status quo in the java world.

  • bottlepalm 2 years ago

    Agreed, checked exceptions are awful. The idealists love them, the realest knows you can't write code up front handle every single exception case, and in most cases you aren't recovering from an exception anyways.

    Especially the higher the level the code is the more potential lower layers that can fail. It's just not scalable to write code up front to deal with every single case. And most programmers don't care about failure, or working around it, just success.

    On the other hand we do expect the unexpected and wrap failure prone code in try/catch so that we can log exceptions and keep the app moving. Figure out later if the ROI is there from enough failures to write special code to try to prevent and/or recover from a particular exception.

    It's pretty much the same as it is in the checked exception world just with more code - in Java people handle the exception, log it and/or rethrow, so what's the point? Idealism and poor assumptions by the language designer. Anyone have any proof that a Java app is more reliable than a C# one?

edpichler 2 years ago

Later you detect an error, more expensive it is to fix it.

  • ars 2 years ago

    That's not really true.

    Vast majority of the time either the caller will fix it, or it simply does not need to be fixed at all it's simply passed all the way up to the top.

invalidname 2 years ago

FYI this too is already supported by Manifold...

_ZeD_ 2 years ago

please, for the love of what's good in the world... NO. checked exceptions ARE A FEATURE, a CORE ONE.

  • dtech 2 years ago

    They are so bad even Java stdlib stopped using them for the most part. Nearly all Java 8+ APIs like streams use unchecked exceptions.

  • lesuorac 2 years ago

    There's a reason using streams with checked exceptions is so painful; checked exceptions are going the way of the dodo.

arein3 2 years ago

In kotlin there are no checked exceptions, and I haven't missed them a bit.

darklycan51 2 years ago

Certified Try catch moment

macpete42 2 years ago

catch them at the source and return sealed classes (used like sum types) instead

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection