Hacker News new | ask | show | jobs
by aiunboxed 976 days ago
This has always been a debate in my head whether to make the function return error codes or to throw those errors.

Irrespective of throwing an error or returning an error you need to handle it somewhere. If you are returning an error then the type system can handle it and the person calling your function can get an idea.. while if you are throwing an error in most of the languages your caller function will not get an idea that this function can throw an error.

4 comments

It's very controversial, but I like Go's approach of treating errors as return values - because that is ultimately what they are.

You can have your language hide exceptions from the control flow if you wish, but they are still things that result from function calls that you should deal with. Why not make them front and center?

You should use asserts or throw errors to prevent misuse of a procedure by the caller. Ideally, you could model these constraints at the type level. You can think of pre-conditions as a sort of pseudo-dependent typing.

Consider the following pseudocode:

    // Idris
    binarySearch : SortedList a -> a -> Maybe Nat
    binarySearch = ?implementation

    // Java
    int binarySearch(List<T> elems, T elem) {
        assert isSorted(elems);
        throw new NotImplemented();
    }
(The generic type parameter should also implement Comparable or similar, but I digress).

In this case, it would be nonsensical to pass an unsorted list to either procedure, so we prevent it - in Idris' case by using dependent typing; in Java's case by using assertions.

To contrast, consider the following:

    // Idris
    readLines : Path -> IO (Either FileError (List String))
    readLines = ?implementation
Here, the FileError represents a number of things that could go wrong: no file existing at the given path, lack of permissions, running out of memory during read and so on. However, this is not misuse of the procedure - these are all expected deviations from the happy path, and hence we model that in the type definition.

(I've omitted a corresponding Java example, as following a similar style would be terribly non-idiomatic. Java prefers throwing and catching exceptions to manage non-happy paths)

Note that we can't constrain the Path parameter to only resolve to files that exist. Doing so would require knowledge of the current filesystem state, e.g. the type signature would be something like the following:

    readLines : (fs : FileSystem) -> (p : Path) -> fileExists fs p -> IO (Either FileError (List String))
    readLines = ?implementation
To summarize, use preconditions, assertions or type-level constraints to prevent misuse of procedures. Return errors (using sum types, for example) when it is known and expected that the result of a procedure can deviate from the happy path. You may refrain from modeling conditions at the type level, even if possible in principle, as it may be prohibitively complex.
>if you are throwing an error in most of the languages your caller function will not get an idea that this function can throw an error

Is this true any more? I feel like most languages, relative to popularity, have compiler-enforced thrown error handling.

C# is one of the offenders. And while Java has checked exceptions, many considered Thema an annoyance and wrap everything in RuntimeException.
Very true. Checked exceptions in Java are a form of function coloring problem for modern libraries.

Especially painful when using any of the functional style stream operations, like `map`, `filter`, etc, or any other higher order function library. Most take in the standard `Function` or `BiFunction` interface arguments, which will not support method referencing for anything which includes a checked exception as part of its signature.

But a better language would allow generalizing over exceptions. In pseudo C++:

  auto map(collection<T> C collection, function<T> F f) -> invoke_result<F, T> throws(invoke_exception<F, T>);
> Checked exceptions in Java are a form of function coloring problem for modern libraries.

So is returning result or error via Either/Expect/Result sum types. Exceptions or Expected, code gets messed up either way.

Not quite of the same caliber. A checked exception has the form `R func(T t) throws Err`. A concrete result type (like Rust has) would look like `Result<R, Err> func(T t)`.

The later fits nicely into the `Function<,>` interface, `Function<T, Result<R, Err>>` vs the former requires a different interface entirely, something like `FuncE<T, R, Err>` where `Err` breaks out into an argument for the throws value of the signature.

Because most of the functional libraries in Java work with Function, BiFunction, etc, we end up with incompatible arguments for common patterns such as `map`, `filter`, etc.

Typescript, Python all are exception
That’s the thinking behind Either (in functional programming, but not only).

TBH, I don’t understand how the absence of checked exceptions or Either in most languages is tolerated!

> the absence of checked exceptions or Either

It seems an unpopular opinion, but I like Checked Exceptions and wish more languages had them. Most of the "problems" people mention about CEs boil down to the damage done by lazy developers who don't want to think of separation-of-concerns/decoupling/layering in their own code, and that can be curbed by better syntactic sugar.