Live data from Hacker News

Away from Exceptions: Errors as Values

humanlytyped.hashnode.dev

91–100 of 145 posts

Re: Away from Exceptions: Errors as Values

#91
post #34

Earlier quoted context omitted.

> For example, Java's parseInt [1] throws a NumberFormatException if the string can't be parsed. IMHO this is terrible design. It's unergonomical design, but it's the _correct_ design: the method is declared to return an int, and it can't fulfill its promise: throwing an exception is the right thing to do.

To me this is not "ecxeptional" at all, as it is easy to call that function with a non number and it should return "normally" that the input was not an integer. I pretty much prefer the rust Result or C++'s expected .

It has nothing to do with "exceptionality". It has to do with the method not being able to fulfill its contract. Rust's returning `Result` is a _different contract_.

Re: Away from Exceptions: Errors as Values

#92
post #54

Earlier quoted context omitted.

I don't think that's true because if I understand it correctly, the return type of functions which can possibly throw unchecked exceptions would not indicate that they can throw or what they can throw. On the other hand, with the "errors as values" approach (including "bubbling up" operators like `?`), you can tell exactly from the function's return type if an error can be returned and if so what the set of possible…

> the return type of functions which can possibly throw unchecked exceptions would not indicate that they can throw or what they can throw As far as I know, that's how Java's "throws" method signature works, which has been widely regarded as a mistake.

Throws is for checked exceptions. Unchecked are not listed in the throws list.

Re: Away from Exceptions: Errors as Values

#93
post #78

> Programming with exceptions is difficult and inelegant. Learn how to handle errors better by representing them as values. Funny how exception were invented because handling errors as values was considered to be tedious. And now, more and more languages are going backward.

I think it's less strange than you think. In most languages that use errors as values, the tediousness is being directly attacked instead of trying to dodge around it. Haskell, in many of its uses, cleans up the tediousness so thoroughly that the code written using errors as values can be almost indistinguishable from code written using exceptions, and yet, nevertheless, the errors are values and no exception machine…

> Haskell, in many of its uses, cleans up the tediousness so thoroughly that the code written using errors as values can be almost indistinguishable from code written using exceptions, and yet, nevertheless, the errors are values and no exception machinery is being deployed.

I don't have experience with Haskell, but I have mixed feelings about monadic error handling in Scala for precisely this reason. It goes to great lengths to recreate the programming ergonomics of exceptions, with exactly the same drawbacks. Monadic error handling, aka "railway-oriented programming,"[0] splits your logic into two tracks: a "good" track, where all your happy path logic lives, and a "bad" track, which is automatically propagated alongside your happy path logic. In my experience, it induces the same programmer mistakes as exceptions do: errors get accidentally swallowed (especially where effects are constructed and transformed,) different errors that require different handling are accidentally treated the same, and programmers fall into the habit of seeing the error track as an inferior, second-class branch compared to the happy path.

It confuses me when programmers (not talking about you, because I don't know how you write code, but people I've worked with personally) bash exceptions and then use monadic error handling to achieve exactly the same trade-offs.

This hasn't turned me off of monadic error handling, but it has made me think of it as FP's version of exceptions, rather than an upgrade. Personally, I think exceptions are a good enough trade-off in most cases, but when you need to be more careful, it is better practice to give all paths the same prominence in code. FP provides a better way to do this: pattern matching. More verbose, yes; harder to spot the happy path when reading code, yes; encourages more careful and thorough thinking about errors, for me absolutely yes. YMMV.

[0] https://fsharpforfunandprofit.com/rop/

Re: Away from Exceptions: Errors as Values

#94
I agree that both exceptions and error values (aka result types) have their place. I would say that error values are good for when a caller should explicitly handle that case, and that exceptions are good for errors that a caller should not be expected to handle explicitly. A lot of times this breaks down as meaningful application errors vs operational or programming errors. I am struggling to find the right words for this, so I can give an example:

Let's say we have a function used to register a new user account on a site like HN.

An error value would be appropriate to return when the username is already taken, so that we can express to the caller that this is a possibility that must be handled. Most likely the caller would want to tell the user. A maintainer doesn't really care when this occurs, since it's part of the application's healthy behaviour.

An exception would be appropriate if the database is unavailable. The caller would not be expected to tell this to the user, nor is there any logical way for the caller to react to this situation specifically. In this example of a web app, the best course of action is likely returning a generic "unexpected error" message and/or a HTTP 500. The caller can typically let the exception propagate to the web layer's top level exception handler where it will be logged. As a maintainer of the system, a stacktrace is valuable for pinpointing the problem with the code path that lead to it.

(Checked exceptions, where available, blur these lines a bit)

---

In the Java world... (stop reading if you don't care about Java) ...it has been increasingly common to see types like Result used for error values. Recently, there have also been additions to the language that make errors-as-values more practical. Sealed classes (a preview feature in Java 16, and a full feature in the soon-to-be-release Java 17) are basically an implementation of product types (with a characteristically verbose Java-ey syntax) that could be used to implement results. Returning to our example with this:

    sealed interface RegistrationResult {
          record Registered(Account newAccount) implements RegistrationResult { }
          record UsernameTaken() implements RegistrationResult { }
          ... 
    }
https://openjdk.java.net/jeps/409

beyond Java 17, you will be able to pattern-match over these with exhaustiveness enforced by the compiler. It will look something like:

    switch(registrationResult) {
            case Registered(Account newAccount) -> ...;
            case UserNameTaken() -> ... ;
            ...
    }
https://openjdk.java.net/jeps/405

Re: Away from Exceptions: Errors as Values

#95
post #20
post #19

No, I don't want to wrap every single statement of my program in its own if-block, thank you very much.

Rust solves this issue by having a ? operator to bubble up Errors. Before that there was the try! macro with the same semantics. That cuts the boilerplate to a minimum while having a well defined and explicit control flow. I agree that if you had to write the ifs by hand it would be a pita. Looking at you, Go.

One could solve the problem further and more conveniently by doing the bubble up implicitly at every call and just re-invent exceptions.

Re: Away from Exceptions: Errors as Values

#96
I am writing some C++ code for a web application, and there I am handling errors via exception. There are two broad types of exceptions, one that is internal and one that needs to be reported to the user. Following is how I an handling the errors, please could you all suggest a better approach if my approach is sub-optimal designwise?

// Base class

    HandleRequest(req, res)
    {
        try {
            try {
                post_processing(req, res) // implemented by derived class
                process_request(req, res) // implemented by derived class
                pre_processing(req, res) // implemented by derived class
            } catch(send_to_user_exception) {
                send_error_to_user(send_to_user_exception.what()) // implemented by derived class
            }
        } catch(internal_exception) {
            log_error(internal_exception.what())
            send_internal_error_to_user(internal_exception.what()) // implemented by derived class
        } catch(unknown_exception) {
            log_error(unkown_exception.what())
            send_internal_error_to_user(unkown_exception.what()) // implemented by derived class
        }
    }
// Each request type is handled by its corresponding derived class and implements the following methods of the base class.

post_processing(req, res) // will throw exceptions of type send_to_user_exception and internal_exception

process_request(req, res) // will throw exceptions of type send_to_user_exception and internal_exception

pre_processing(req, res) // will throw exceptions of type send_to_user_exception and internal_exception

send_error_to_user(error)

send_internal_error_to_user(error)

Re: Away from Exceptions: Errors as Values

#97
post #34
post #30

I'm firmly in the camp that believes that exceptions are a false economy. The post links to an "Exception Smells" post that doesn't mention one of my pet peeves: exceptions as control flow. For example, Java's parseInt [1] throws a NumberFormatException if the string can't be parsed. IMHO this is terrible design. As a side note, checked exceptions are terrible design. I wrote C++ with Google's C++ dialect where excep…

> For example, Java's parseInt [1] throws a NumberFormatException if the string can't be parsed. IMHO this is terrible design. It's unergonomical design, but it's the _correct_ design: the method is declared to return an int, and it can't fulfill its promise: throwing an exception is the right thing to do.

Java needs a TryParseInt (sorta like C# has) so can you use either one as appropriate.

There are actually two main use cases for integer parsing: one where the value is expected to be an integer (you're parsing a file format) and the other where it's just likely not to be an integer (getting input from the user).

Re: Away from Exceptions: Errors as Values

#98
post #93
post #78

Earlier quoted context omitted.

I think it's less strange than you think. In most languages that use errors as values, the tediousness is being directly attacked instead of trying to dodge around it. Haskell, in many of its uses, cleans up the tediousness so thoroughly that the code written using errors as values can be almost indistinguishable from code written using exceptions, and yet, nevertheless, the errors are values and no exception machine…

> Haskell, in many of its uses, cleans up the tediousness so thoroughly that the code written using errors as values can be almost indistinguishable from code written using exceptions, and yet, nevertheless, the errors are values and no exception machinery is being deployed. I don't have experience with Haskell, but I have mixed feelings about monadic error handling in Scala for precisely this reason. It goes to grea…

> This hasn't turned me off of monadic error handling, but it has made me think of it as FP's version of exceptions, rather than an upgrade.

This reminds me a lot of Java's checked exceptions just with different window dressing. You move the failure mode type information from the exceptions list ("throws" clause) into the return type.

Typed error return values is definitely an improvement in ergonomics over C-style error code returns, and pattern matching is definitely a big improvement on ergonomics too, but I think error handling has a problem of fundamentally irreducible complexity. For example, if you make network calls, you have to be prepared for network calls to fail, and you have to design your system to recover from it somehow, whether that happens in a try/catch or a match on Either[Throwable, Result].

Re: Away from Exceptions: Errors as Values

#99
post #56

Earlier quoted context omitted.

> The compiler should force you to handle every exception in some way, or to check for it. This is the single most unproductive mis-feature a language could have for me. Programming is already a tedious excercise of wrangling your thoughts into an alien form the computer can understand. You want, on top of everything else, the computer to refuse to run your program at all, unless you explicitly handle every possible…

I don't quite follow. You always have to somehow handle the case the file does not load successfully. In exception languages that handling might be implicit (raise an exception and crash your program) and in "errors as values" languages you at least have to acknowledge that it could go wrong with something like `image.unwrap()` (which turns it into a program aborting panic).

One of my personal favorite examples of exception handling was a small GUI app with a single top-level exception handler at the event loop that displayed an error message and continued.

That application was extremely robust. You try and save a file and 100 different things could go wrong (network drive unavailable, file is read-only, etc) but it nicely recovered and you could see what the problem was, correct it, and re-save. One single exception handler for the whole app.

Re: Away from Exceptions: Errors as Values

#100
post #96

I am writing some C++ code for a web application, and there I am handling errors via exception. There are two broad types of exceptions, one that is internal and one that needs to be reported to the user. Following is how I an handling the errors, please could you all suggest a better approach if my approach is sub-optimal designwise? // Base class HandleRequest(req, res) { try { try { post_processing(req, res) // im…

There's nothing wrong design-wise with your approach, IMO. I've seen several people (including very well-known C++ personalities) argue that exceptions should be used for X and error codes for Y, but this is just convention.

C++-wise, you probably want to catch std::exception and "..." too.

Finally, you said that there's two types of exceptions and only one of them is supposed to be reported to the user, but in your code you seem to report everything to the user. You should edit your message to clarify what you meant.

Post reply on HN