Follow

programming poll, boosts welcome :boost_ok: 

What do you think is are good error handling mechanisms?

Multiple choice poll, boosts welcome for reach.

Explanations of the terms will follow shortly.

Explanations 

Exceptions are a mechanism where a function/method aborts execution, and all callers abort too, up to the one who defined a handler (often in form of a "catch" or "except" block), which also receives information in form of an exception object which usually contains a stacktrace (a list of callers of functions down to the location where the exception was thrown).
Exceptions are used for example in Python, Java, C++ and C#.

Sum types are data types where the value can be either of multiple variants (compared to product types where the value consists of multiple fields at the same time). Typical sum types used for error handling are Either (also known as Result) and Optional (also known as Option or Maybe). An Optional value can either contain a value or an empty variant, while an Either type can contain either a value of one variant or a value of another variant. In case of an error, a function using Either returns a value of the other variant while a function using Optional returns the empty variant. The caller usually uses pattern matching to branch between using the result value and handling the error.
Sum type error handling is used in Haskell and Rust.

Multiple return: Some programming languages allow a function to return multiple values. This can be used for error handling in two ways. Either the function returns a boolean (yes/no value) indicating whether an error occurred together with the result value, or an error value and a result value, where the not applying one is null.
The boolean + value version is used in Lua and C# (in C#, the boolean is usually the sole "actual" return value while the result is returned via an output parameter), while the error+ result version is used in Go.

Erlang and Elixir use a mechanism somewhere between multiple return and sum types, where a tuple is returned that either contains an "ok" symbol and a result value or an "err" symbol and an error value.

Null: In many programming languages there's a null value (also known as undefined, nil or None). If this is used as an error handling mechanism, the function returns null in case of error. A drawback is that this is usually not marked in the function's signature and needs to be explicitly remembered to be handled by the caller, and trying to call methods on a null value causes another, unintended error, the dreaded "NullPointerException" in Java or the "NullReferenceException" in C#. In C and C++ it can even lead to hard program crashes.

Reserved value: In this system, a specific value within the return type of normal results is returned, for example a negative number. The drawback is that the value has to be explicitly remembered to be checked, and it is not obvious from the signature. This leads to further, worse errors when that value is accidentally treated as a success return.

Global variable: In older programming languages like C, a function that can experience an error might store a value in a global variable like ERRNO. The drawbacks are that that variable has to be remembered to be explicitly checked, and it causes problems when a program uses threads (doing multiple things at the same time within the same process) because then the global variable might be set on another thread in parallel.

My favorite is sum types, because they explicitly put the fact that a function might error in the return type. For errors that are infrequent or clearly caller error, exceptions are good.

programming poll, boosts welcome :boost_ok: 

@LunaDragofelis Monads on Sum-Types with programming-language supported syntactic sugar for bind-notation.

programming poll, boosts welcome :boost_ok: 

@LunaDragofelis depending on the chosen monad/sum-type they can handle all the sensible cases mentioned:

Exceptions: ExceptT (Aka: Either Excepiton a)
Null: MaybeT (Aka: Maybe a)
Global Variable: StateT (Aka: State variableStorage a)

multiple returns would not be typesafe and reserved values is just a big nono because you mix semantically different types into one.

Best thing:
You can abstract "failure state" on the monad, making the code generic.

programming poll, boosts welcome :boost_ok: 

@LunaDragofelis
Example:
myThingThatErrors :: FailState m => m a
myThingThatErrors = .....

now you can instance m to be either of the above cases WITHOUT touching "myThingThatErrors" .. so you use the same function in ANY context the caller chooses & wants :)

(maybe = run, i don't care about the errors; either: run and report errors; state: run & manipulate things in the background).. :)

programming poll, boosts welcome :boost_ok: 

@LunaDragofelis @Vierkantor i just stumbled again on "TheseT a b":
github.com/haskellari/these
The monad for that is "ChronicleT" at the bottom.

Basically a Monad with 2 failure modes:
- continue, but log on the side (i.e. when parsing as much as possible)
- bail out & abort hard (i.e. on "real" error)

@LunaDragofelis That is indeed a "good start".

Now just sprinkle monads on top (can be done "straightforward" in javascript, with a crutch in most other languages (pymonad, scala, ..) or you just start using haskell :D )

Then you "just" have to code the "happy path" & things get collected on the side ..

I attached some example code of a project i currently work on.

Includes: heavy IO-Interaction, safe cross-thread-concurrency, data verification, complete error handling.

@LunaDragofelis one of the issues with exceptions in many languages is that you can't be entirely certain the exception you caught came from the code you expected (instead of being propagated from some nested call to some other code you're not aware of).

@meganeko yeah, exceptions should be used in cases where they aren't ordinarily expected to be caught, and will be handled by a generic error handler.

@LunaDragofelis One fancy trick (which is similar to exceptions once you ~~hack your brain~~ ~~transcend the imperative plane~~ understand continuations) is the following: instead pass an extra argument to the function, which gets called in an exceptional condition.

For example, I have a `lookup-or' function that takes 3 parameters: a container to do the lookup in, the key to look up and an `or-else' function. If the key is found, `lookup-or' returns the value associated to the key. If the key is not found, `lookup-or' instead returns the result of calling `or-else' on the container and key. You basically get the effect of exceptions (since you can just raise an error within the `or-else' function), but with the additional possibility to return a default value instead.

@Me_but_lewd @LunaDragofelis Thanks! Your reply actually didn't federate yet so I'm happy that I wasn't repeating anything you already mentioned :D

@LunaDragofelis Having gone through all the options listed, in practice the best thing is to always return an error code. If you have other outputs, return them in out parameters, which makes this a multiple return kind of scenario.

Optional types lose information on *why* something didn't get returned. It turns out that in the majority of cases, there is a choice of different reasons, and as a function author you can't always predict when knowing this reason is unnecessary.

@LunaDragofelis The broader lesson here is that a boolean success/failure state is easy to report, and easy to handle, but only in the simplest of cases.

Sometimes you can return multiple failure modes with multiple reserved values. But boolean, null or optional can't offer that.

Exceptions are good because they report multiple error states and force handling, but they're bad because handling is clumsy.

The best I've found are mandatory return types which return a code.

@LunaDragofelis Mandatory return types *must* be handled, but with the usual control flow, or explicitly ignored.

They can be used in C++ much like optional types, and can assert or throw at runtime if the code was not handled/ignored. It's hard to implement this in languages without RAII, though.

@jens @LunaDragofelis I really like the plan 9 way. The function returns an error value like nil (0), -1, or whatever (depends on the function), and it always sets the global error string (errstr). There are print functions that can handle the errstr implicitly. A sample use would look like this:

char* samplestr;
if (!myfunc())
sysfatal("error with string %s: %r", samplestr);

It's quite clean, imo.

programming poll, boosts welcome :boost_ok: 

@LunaDragofelis for me when operating in an "functional core, imperative shell" I find exceptions are fine if they are strictly used in the thin imperative shell outer layers of the code, and the bulk of the code should be side-effect-free and represent failures as first-class values. sum types if you have 'em obviously, but Erlang shows that you can benefit a lot from that style even without static types, as long as you have pattern matching built-in.

re: programming poll, boosts welcome :boost_ok: 

@technomancy @LunaDragofelis i'd say the opposite: erlang is riddled with exceptions. pattern matching only works as a sum-type-alike there because if something doesn't match, it'll generate an exception, and 95 times out of 100, that'll crash.

if not for erlang's powerful process and supervisor system, it'd have all the normal downfalls of dynamic language error checking; but its ability to crash and recover makes it far more robust

re: programming poll, boosts welcome :boost_ok: 

@bjc @LunaDragofelis sure; I only meant that in a dynamic language if you try to represent failures as values, you will fail miserably unless your language includes pattern matching in the core the way erlang does. you see "dynamic sum types" used everywhere in erlang... clojure has many such attempts at doing the same but they all suck because Rich Hickey hates pattern matching.

you certainly can't generalize around the way erlang does "let it crash" error handling because erlang without OTP is only half the story, and no one else (except elixir) has OTP.

programming poll, boosts welcome :boost_ok: 

@LunaDragofelis
Common Lisp's condition system (exception-like but far more flexible) is the best error handling system I've ever used: gigamonkeys.com/book/beyond-ex

Barring that since AFAIK that condition system is unique to Common Lisp, any error handling system that doesn't allow you to blithely ignore errors. So that NAKs 99.99999% of “multiple return value” systems since you can typically ignore secondary values. Sum types only if the error must be handled or at least acknowledged, even if you're not otherwise using the function's return value (Hare and Zig are good examples of being able to mark parts of the sum type as errors so they must be dealt with).

*Maybe* NULL if there's NULL dereference safety in the language and the function is only useful when using the return value (e.g. malloc).

re: programming poll, boosts welcome :boost_ok: 

@LunaDragofelis I voted for sum types and "something else" (rephrase the function into something that cannot "fail"; for example, never use + and handle the overflow when using clamping_add or wrapping_add would have sufficed).

programming poll, boosts welcome :boost_ok: 

@LunaDragofelis exceptions create un caught errors without anyone knowing, Languages that use exceptions keep me up at night.

re: programming poll, boosts welcome :boost_ok: 

@LunaDragofelis

I wrote a *massive* response to this and then accidentally deleted it. :ablobcatcry:

I might rewrite it later, but TL;DR: exceptions are the only acceptable solution. Hot take, I know, but I have solid reasons to say this. I'll have to rewrite my actual response later to explain.

re: programming poll, boosts welcome :boost_ok: 

@LunaDragofelis

OK so here goes.

All options other than exceptions force the programmer to handle the error immediately where it happened. That's not a good thing at all. It would only be acceptable if the amount of boilerplate needed to say "if this returns an error, then return it from here" is really tiny, like a single character after a function call or something...

Imagine you're writing a long procedure that does a lot of back-and-forth talking to a server, like a batch update that has to be rolled back or at least immediately aborted when an IO error happens at any point due to network connectivity.

If every time you called a function that sends data, you had to explicitly check its return for a sum type / second return value / reserved value / null / global variable, your code will become incredibly repetitive. The problem applies not only to the top-level procedure but also to any sub-procedures it might delegate part of the task to:

```
void do_updates() {
r = send_prelude();
if (is_error(r)) return r;

r = begin_update();
if (is_error(r)) return r;

r = send_data(xyz);
if (is_error(r)) return r;
}

void send_prelude() {
r = send_data(header_foo);
if (is_error(r)) return r;

r = send_data(header_bar);
if (is_error(r)) return r;
}

void begin_update() {
r = send_data(header_update);
if (is_error(r)) return r;
}

void send_data(); // system function, may return error
```

If you think this is necessary for being able to reason clearly about the control flow, or that exceptions mess up the concept of strict static typing, that's actually wrong. Super-strictly statically typed languages could learn a lesson from Java (yes! shock!) and implement *checked exceptions*. This means that the exceptions a function may throw are part of its signature, and calling a function that may raise Exception X means that you either handle it locally, or add Exception X to your own signature as well.

This way, the static type system knows about the exceptions that may pass through any part of the control flow, and raise a compilation error if there's a mistake:

```
void do_updates() throws error_x {
send_prelude();
begin_update(); // COMPILATION FAILED: error_y is neither handled nor thrown
send_data(xyz);
}

void send_prelude() throws error_x {
send_data(header_foo);
send_data(header_bar);
}

void begin_update() throws error_x, error_y {
send_data(header_update);

if (something_is_wrong())
throw error_y(blah);
}

void send_data() throws error_x; // system function
```

re: programming poll, boosts welcome :boost_ok: 

@LunaDragofelis

Shoot, I forgot to declare that post as MarkDown. If it's unreadable, please tell. The indentation actually rendered fine on PleromaFE...

@LunaDragofelis I am against global variables, but the other options can be appropriate. I also would add callbacks.

programming poll, boosts welcome :boost_ok: 

@LunaDragofelis voted for everything, i don't think the actual mechanism matters much. some will be better suited to different situations and usecases than others.

imo the hard part of error handling isn't detecting that an error has occurred, it's deciding what to do with it.

figuring out which errors should log warnings, which should crash, which should be handled and recovered from, and so on is very difficult.

Sign in to participate in the conversation
Embracing space

The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with Mastodon!