Author Topic: Error handling [proposal]  (Read 4490 times)

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Error handling [proposal]
« on: December 05, 2018, 02:34:37 PM »
The main idea is to pass results using an error channel that is separate from the underlying result. The call is compatible with C.

A return value can be described as:

Code: [Select]
struct _result {
   union {
     Result result;
     Error error;
   }
   bool error;
}

Where "Result" is the result type which will vary by method.

For example, consider a method createFoo() that could return an error or a Foo*. The C signature would look like this:

Code: [Select]
struct _resultFooStar {
   union {
     Foo* result;
     Error errorResult;
   }
   bool error;
}

struct _resultFooStar createFoo() {
   ...
}

For C it can then be used in this manner:

Code: [Select]
struct _resultFooStar res = createFoo();
if (res.error) {
  // Do something with res.errorResult
  return;
}
Foo *foo = res.result;

For C2 we obviously want some more syntactially pleasing. The tricky part is to assign both error and normal result, even though they are exclusive.

Go would write something like foo, errorResult := createFoo() but this still requires an extra test and is something they actively work on to improve. Not to mention that C2 doesn't have tuples.

Keyword or not?

We can decide on using keyword (catch, rethrow) or use symbols, e.g. !!, !? etc. Below I will offer both versions with keywords and without keywords.

Rough common ground

First of all we need a way to rethrow any error to the top. In Go this would be:

Code: [Select]
res, err := createFoo();
if (err) return nil, err;

Zig on the other hand uses "try" to signal that it will rethrow:
Code: [Select]
res = try createFoo();

Here I suggest we use the same method as Zig, but with postfix:

Code: [Select]
res = createFoo()!!; // !! to signal rethrow.
res = createFoo() rethrow; // using keyword instead.
res = createFoo(); // compile time error since error is not handled.

Secondly, we need a way to ignore the error and replace it with a dummy value if there was an error.

In Zig:

Code: [Select]
res = createFoo() catch defaultFoo; // Return defaultFoo in case of error.

For C2 I propose the following:

Code: [Select]
res = createFoo() !! defaultFoo; // The idea is using !! as "||"
res = createFoo() ?! defaultFoo; // Instead borrowing from ?: syntax
res = createFoo() catch defaultFoo; // Using keyword catch
res = createFoo() else defaultFoo; // Using keyword else

Different possibilities

For handling errors we have two major directions (that actually can be used together). One is using error handlers defined separately, the other is handling direct at the callpoint.

We can look at Zig's handling:

Code: [Select]
if (createFoo()) |foo| {
  doSomethingWithFoo(foo);
} else |err| switch (err) {
  error.FooCreation => {
    // handle this...
  },
  // handle next error here ...
}

In the Go2 proposal we have handlers:

Code: [Select]
handle err {
  if (err == "FooCreation") {
     // handle this
  } else {
    // Handle other error
}

foo := check createFoo()

C2 errors with inline error handling (like Zig)

Code: [Select]
// Using !!
FILE *f = fopen("Foo") !! (err) {
  // Switch-like structure opened by default
  case error.FooCreation:
    // handle this...
  case ...
   // handle next error here ...
};

// Using catch keyword
FILE *f = fopen("Foo") catch (err) {
  // Switch-like structure opened by default
  case error.FooCreation:
    // handle this...
  case ...
   // handle next error here ...
};

// Using !=> with error variable being implicitly defined (does not allow nesting)
FILE *f = fopen("Foo") !=> {
  // Switch-like structure opened by default
  case error.FooCreation:
    // handle this...
  case ...
   // handle next error here ...
};

The downside of the above is when wanting to handle things in an if statement. Consider:

Code: [Select]
while (Foo* foo = createFoo()) {
   ...
}

Now envision error handling of the above type. Not pretty. Here error handlers shine.

C2 errors with error handlers

Code: [Select]
handler errHandler(err) {
  case error.FooCreation:
    // handle this...
  case ...
   // handle next error here ...
}

Foo *foo = createFoo() catch errHandler;

while (Foo* foo = createFoo() catch errHandler) {
   ...
}
;

This also allows for reuse of handlers at several points in the code.

Note that they are not mutually exclusive, they can actually coexist.

Examples

Finally I end with a few examples that can be used to imagine real life situations:

Rethrowing or using defaults:

Code: [Select]
if (a_may_throw() rethrow + 2 > 0) {
    // do something
}

if (a_may_throw()!! + 2 > 0) {
    // do something
}

if ((a_may_throw() !! 0) + 2 > 0) {
    // do something
}

if ((a_may_throw() ?! 0) + 2 > 0) {
    // do something
}

(I'll add some more later)

Proposals rules out

I considered a solution like this:

Code: [Select]
FILE *f ! err = fopen("Foo");

// read(f) - Would be compiler error
// as err is not tested in the scope

if (err) {
    printf("Found error! %s", err);
    raise err;       
}   

// f can be used here since err is checked

But it did not seem attractive enough compared to the other methods (inline and error handlers), and it also requires more analysis and special syntax / grammar without adding clarity.

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: Error handling [proposal]
« Reply #1 on: December 05, 2018, 05:03:22 PM »
C-compatibility

From C, it is possible to use the generated error structs. Some additional macros can be written to ensure even smoother usage.

Calling C, it can be useful to have a seamless conversion of result-code based calls (1), errno-calls (2) and calls with error as in-parameter (3) e.g:

Code: [Select]
// 1
API_RESULT do_something(int a, Foo *foo);

// 2
bool success = do_something(int a, Foo *foo);
if (!success) { /* do something with errno */ }

// 3
Error err;
bool success = do_something(int a, Foo *foo, &err);
if (!success) { /* do something with err */ }

All of these could be automatically wrapped by C2 later on. However, it's not important for the first version.

Function signatures

Zig uses ! to denote errors or error separator, e.g:

Code: [Select]
pub fn parseU64(buf: []const u8, radix: u8) !u64 { ... } // inferred error
pub fn parseU64(buf: []const u8, radix: u8) anyerror!u64 { ... } // catchall
pub fn parseU64(buf: []const u8, radix: u8) ErrorSetX!u64 { ... } // throws errors from the ErrorSetX enum.

For Go it's just part of the signature.

For C2 there are a bunch of solutions:

Code: [Select]
func u64 parseU64(u8* buf, u8 radix) !anyerror { ... }
func u64 parseU64(u8* buf, u8 radix) anyerror!!  { ... }
func u64 ! anyerror parseU64(u8* buf, u8 radix)  { ... }
func u64 !! anyerror parseU64(u8* buf, u8 radix)  { ... }
func u64 parseU64(u8* buf, u8 radix) fails(anyerror) { ... }
func u64 parseU64(u8* buf, u8 radix) throws anyerror  { ... }
func u64, anyerror parseU64(u8* buf, u8 radix)  { ... }
func u64, anyerror!! parseU64(u8* buf, u8 radix)  { ... }
func u64 parseU64(u8* buf, u8 radix) anyerror!!  { ... }

Payload
I believe the idea of Zig to simply make the error code an enum union is good. Unlike other schemes, this means that adding additional error codes does not break the signature. The enum union should be register sized but at least 32 bit, meaning 64 or 32 bits in practice.

Encoding additional information some further discussion, there are essentially 3 approaches:

  • No additional data, just the enum
  • Error is encoded in the result
  • A thread local errno can encode information

(3) breaks purity of functions.
(2) changes size in return object if the error payload is allowed to vary. If (2) returns a pointer then cleanup of that pointer might be hard to enforce.
(1) Is limiting, and will probably mean ad-hoc solutions passing in &err-structs.

I'm slightly partial to (2) despite the drawbacks.

bas

  • Full Member
  • ***
  • Posts: 220
    • View Profile
Re: Error handling [proposal]
« Reply #2 on: December 07, 2018, 10:40:23 AM »
The main issue I have with this approach is:
- what is the actual problem it's trying to solve?
- it makes the syntax a Lot more complex (and unreadable).
- it only helps if the error system is used universally used (like in Go). In C that will never be the case

I think the power of C(2) is it's simplicity. Code can be very readable, because once you understand
a few simple patterns, it's all the same. Like:

Code: [Select]
int fd = open(..);
if (fd == -1) {
   // error
}

So in C the pattern is to (for example) use signed values to be able to use special values (eg -1) to indicate
an error.

A more extended pattern is to always return a Result Enum, where Result_Ok is the happy flow. Other output
variables then need to be passed back via out params.

Just sticking to these patterns is so very powerfull, since all C programmers understand them from day 1.

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: Error handling [proposal]
« Reply #3 on: December 07, 2018, 10:49:58 AM »
I understand the scepticism. But let’s see if we can refine the proposal syntax wise first? I’ll adress the practical issues if we can create something syntactically simple.