Live data from Hacker News

Away from Exceptions: Errors as Values

humanlytyped.hashnode.dev

121–130 of 145 posts

Re: Away from Exceptions: Errors as Values

#121
The way I see it, the issues with Exceptions are with 1) types and 2) the try/catch syntax, and the issue with Error-As-A-Value is that it's cumbersome.

Exceptions solve a very real problem. Sometimes I get to the point where there's nothing more I can do and it's time to start unwinding the stack. I eventually either signal with try/catch that I'm ready to start handling the issue somewhere up the stack or never do and I crash.

Error-As-A-Value addresses the "types" problem (specifically Option types do this; Go ignores this problem AFAIK and errors are poorly supported by the type system) and "forces" users to be explicit, except they can always just ignore the value when they need to anyway but now with added boilerplate. Just as importantly, they propagate this boilerplate to any caller, even if the caller doesn't care. Having to say within each and every caller, no, I really don't care about this error and there's nothing I can do about it right now is tedious, cumbersome, and often truly introduces no value.

I think we can do better than either by allowing the use of both. What if I had the compiler and other tooling keep track of the Exceptions that can be thrown?

    const RandomError = new Error("you have bad luck!")
    const DivideByZero = new Error("cannot divide by zero!")

    // this can only throw RandomError
    const maybeAdd = (a: number, b: number): number => {
        if (randrange(0, 1) > 0.5) throw RandomError
        return a + b
    } ?? RandomError

    // myFun can throw RandomError or DivideByZero, and our tooling
    // will help us keep track of that.
    const myFun = (a: number, b: number): number => {
        if (b === 0) {
            throw DivideByZero
        }
        return maybeAdd(a, b) / b
    } ?? DivideByZero
Well, in TS/JS, now I still need to use try/catch at some point to handle the exceptions this will eventually throw. But maybe an error-as-a-value makes more sense. What if I included sugar that optionally replaced try/catch with error-as-a-value, if that's what the use case called for?

    type Result = {
        ok: number
        error: DivideByZero | RandomError
    }

    // myFun(1, 2)? will return the result type indicated above
    let { ok, error } = myFun(1, 2)?
    while (!ok) {
        { ok, error } = myFun(1, 2)?
    }
    return ok
This is an unfortunately contrived example but I think it demonstrates my point. I don't really see any reason we can't have both in modern languages.

1. The problem with "types" in Exceptions being that you usually don't have any insight into whether or what errors can be thrown in a language that uses Exceptions as the main error handling control flow

2. The problem with try/catch syntax is subjective, but sometimes you don't want to introduce new scopes and at least 4 new lines. And code with extensive error handling becomes unnecessarily littered with try/catch when you would have preferred an abbreviated assignment expression as with error-as-a-value.

Re: Away from Exceptions: Errors as Values

#122

There's a place for both. Error values for conditions that your client will want to handle, and exceptions or panics for all the fatal failures.

I don't really understand the place of panics. It's not really up to a function to determine whether its error is unrecoverable (especially in the case of a library), it's up to the caller. And an unhandled Exception is, practically speaking, a panic. So it seems to me that Exception covers both use cases.

Re: Away from Exceptions: Errors as Values

#123
post #105

Earlier quoted context omitted.

The main caveat is that oftentimes, checked exception handling doesn't compose well - see what kind of trouble Java gives you, for example. Recent articles I've read on effect modeling languages seems to give a more uniform construct for bringing checked exceptions in line with other control constructs.

> see what kind of trouble Java gives you, for example. I program a lot in Java, and the only trouble there is the lack of support for sum types and/or variadic type parameters in generics (i.e. to express functional interfaces that can throw an arbitrary number of checked exceptions, as a type parameter). That’s the only pain point for me and is something that could be fixed. In fact, the interplay with control stru…

I mean, syntactic sugar is one thing, but being entirely incapable of creating variadic exceptions really hamstrings you in places where you don't wish to write a great deal of redundant code for each combination of exceptions you may see.

Re: Away from Exceptions: Errors as Values

#124
post #69
post #41

Earlier quoted context omitted.

Can you elaborate then?

I did in the original post, and that's why I used C++ as example instead of Java. A method declares `throws X, Y, Z` and _runtime_ checks that no other exception escapes. No source changes needed if you add W to the list. And if some other exception escapes, it's wrapped in `UnexpectedException` that is reserved for and throwable only by the runtime.

I guess we could extend Project Lombok to do this. You'll still have your "throws" statement but then the tool would wrap calls to catch those that are not listed in "throws" statement and wrap them as you say.

Re: Away from Exceptions: Errors as Values

#125
post #93

Earlier quoted context omitted.

> 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 e…

I think trying to reduce the complexity of error handling is the original sin. Thinking of error handling as a separate case requiring separate mechanisms is the original sin. The structure of your code should not reflect any difference between "error" and "success" cases. They are equal, and neither should be subordinated to the other.

Re: Away from Exceptions: Errors as Values

#126
post #77

Earlier quoted context omitted.

> when you start introducing other factors like how to report the errors publicly to a non-technical user, maybe in different languages, or whether to log it or send it who knows where, whether to trace or not, how, how to deal with duplicates or similar errors... I’ve tried searching for articles that talk about people deal with this in the context of web apps but have found it difficult to find content. It’s a tric…

I recommend again reading the article I linked to. The answer to the first part is: this should be an exception (or abandonment, as the Midori team called it). This is an error in the logic of the code, it's a programming error (even if it's due to later changes or whatever). It's an error that needs to be fixed in the code, not "recovered from". Now, you can also catch exceptions, indeed. You could have your app cat…

I appreciate the write up. I’ll also checkout that article. Thanks!

Re: Away from Exceptions: Errors as Values

#127
post #31

> Some errors are unexpected and should stop the program; you want to use exceptions for those. Precisely the opposite: exceptions are a fail-fast mechanism that gives you an alternative to terminating the program. Now, as slx26 mentioned, it's only half of the story. Most APIs (including .NET) document exceptions badly, they're not discoverable, and if you try to use them to _recover_ from a condition, you're in for…

There's no need for even that complexity. If base exception type had a single property, something like "CanRetry", all exception handling would be simple. Because when it comes to exceptions there are really only 2 things you can do: abort the current operation or retry it. The code generating the exception will know which of these is appropriate and the try/catch handler is what would restart or cancel the operation…

> If base exception type had a single property, something like "CanRetry",

"Can retry" _WHAT_? This can work if you meticulously rewrap low-level exceptions into higher-level ones that reflect the high-level operation that failed.

Concrete example: FileNotFoundException. I'd say that in "normal" circumstances it's not retriable: you're looking for a file, it's not there, so an exception is thrown. In "unusual" circumstances you're polling (i.e., waiting for a file to appear somehow) or you're an OS shell and is searching the path for the location of the program.

Re: Away from Exceptions: Errors as Values

#128
post #124
post #69

Earlier quoted context omitted.

I did in the original post, and that's why I used C++ as example instead of Java. A method declares `throws X, Y, Z` and _runtime_ checks that no other exception escapes. No source changes needed if you add W to the list. And if some other exception escapes, it's wrapped in `UnexpectedException` that is reserved for and throwable only by the runtime.

I guess we could extend Project Lombok to do this. You'll still have your "throws" statement but then the tool would wrap calls to catch those that are not listed in "throws" statement and wrap them as you say.

That'd be cool.

It just occurred to me that I could probably do the same with attributes and DynamicProxy for C#. And also implement additional checks like "IF X is thrown, its properties must satisfiy some constraints.". (I program both in Java and C# these days.)

Such "UnexpectedException" as I suggested would serve two purposes: 1) well, knowing that something unexpected happened and allowing you to handle it with "last chance handler", 2) helping the developers maintain the contract. If you change a method so that it can throw some new exceptions (compared to the previous version), you've broken its contract/compatibility. This would then show up during testing.

> You'll still have your "throws" statement but then the tool would wrap calls t

Yes. Fortunately, Java allows you to mention subclasses of RuntimeException in "throws" declaration.

Re: Away from Exceptions: Errors as Values

#129
post #72
post #71

Earlier quoted context omitted.

> Note that in Kotlin the return type is `Int?`, not `Int`. How does that fare with unnecessary boxing of primitives?

If the function isn't total (as in: for every string there is an int) then the boxing is necessary, no?

Not if you have user-defined value types (soon coming to Java).

Re: Away from Exceptions: Errors as Values

#130
post #19

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

Not sure why people don't like exceptions. Throw different error classes according to the source of the problem and just handle differently in the upper classes.

throw new ErrorUser('Bad input')

-> Show friendly error messages.

throw new ErrorFatal('Db unavailable')

-> Email error to dev and quit.

I never like the verbosity of returning errors from each methods.

How hard is it to trace the stack when you're supposed to be using error logging tools like Sentry?

Post reply on HN