~skeeto/public-inbox

1

Error handling macros

Details
Message ID
<fdfbd6d2-c4a1-5338-bf65-2bf39f3801ff@danielas3rtn54uwmofdo3x2bsdifr47huasnmbgqzfrec5ubupvtpid.onion>
DKIM signature
missing
Download raw message
Do you have any kinda system for dealing with errors in C? I looked at
some of your code and you seem to be using the libc convention (if one can
call it that) of returning 0 on success and when it comes to structs
returned by value you use .ok/.err fields. For context, I personally
prefer to return a true value on success and I use by-value structs mostly
as fat pointers where the first field is always the 'lean' pointer, which
is null on error. Whatever the system, there's often a need to propagate
error. I have several macros for this, e.g.

Window* W = OK( Create_Window() );

If the expression passed to OK() produces 0, OK() returns an error result
from the current function, otherwise it returns the result of the
expresion. Another example:

Node  P = SOME( Parent_Of( N ) );

Node here is a fat pointer type and SOME() checks if the lean pointer
inside of it is nonzero. Together these macros allow for writing more
compact code while still handling errors properly. I even cobbled together
a macro called PIPE() that takes a sequence of expressions and has
semantics similar to `f() && g() && h() && ...` where each function
returns a fat pointer. There's more going on with these macros but that's
the gist. Problem I've run into is that in order to return an error result
(i.e. a zero scalar or some kind of zeroed struct) I need to know the
return type of the current function when inside its body. I had to resort
to a hack so now I define functions like so:

fun( int,Sum, int A, int B )  return A + B; end

Which is about as fugly as it gets, hence this email. Have you ever
experimented with this kind of stuff? I know that in Rust and Zig there
are conveniences specifically for error propagation. Error handling in
general is one of those perennial problems with C, but unlike string
handling where pretty much everybody sane uses some kind of {.Data, .Size}
type, error handling is all over the place.
Details
Message ID
<20231005235943.i4lq7buqofofares@nullprogram.com>
In-Reply-To
<fdfbd6d2-c4a1-5338-bf65-2bf39f3801ff@danielas3rtn54uwmofdo3x2bsdifr47huasnmbgqzfrec5ubupvtpid.onion> (view parent)
DKIM signature
missing
Download raw message
> I looked at some of your code and you seem to be using the libc 
> convention

Thanks for taking a look! Programs using the old libc convention would be 
older, and .ok/.err is the newer stuff. I've got a nearly-complete post in 
the works about this coming out soon. In short, I've gravitated towards 
returning structures instead of in-band signaling and out parameters. It's 
like a form of multiple return values. Once I got over that hump, using an 
.ok boolean to signal errors was easy. The main difference between .ok and 
.err is whether a zero-initialized object ought to be invalid or valid.

With .ok I can zero-initialize a return value at the top of a function, 
and if there's any error I just return it as-is. A success return must 
flip it to true. In more complicated code there are few such cases, and 
forgetting to flip it will be caught quickly (being the happy path). I'm 
currently quite happy with this situation, and, combined with no longer 
checking for allocation failures, I don't feel an urge to improve error 
propagation beyond this.

I bet this idea would go well with your macros if you consistently name 
this return value. The macro could always return "r" (or whatever) knowing 
that it reliably defaults to an error state.

Personally I don't like macros the way you describe. I like control flow 
out in the open, not hidden, and especially no hidden return statements. 
I'm also one of those people who _likes_ explicit "err != nil" in Go. In 
most cases when I see complaints, the real issue is too many accidental 
error paths. For example:

* Validating arguments and returning errors instead of assert/panic. This 
is surprisingly common.

* Propagating such checks from libraries. For example, in C checking the 
result of pthread_mutex_lock. It's a misunderstanding of the interface. Of 
course it's painful to introduce error paths every time a program touches 
a lock.

* Managing resources at the wrong level, or holding them for too long. For 
example, people write C as though it's still the 1980s and a .ini file is 
too large to reliably fit in memory. So they hold a file handle and do 
lots of reads on it, any of which could fail, requiring a handle close on 
each case. Just load the whole file into memory! Use that 64-bit address 
space to your advantage.

* Using libraries with crummy interfaces that needlessly demand resource 
and/or lifetime management, which has combinatorial effects on all the 
error paths. (Often as a result of the mistakes above.) Annoyingly common, 
and while the right answer is to replace it with something better, that's 
easier said than done. Recent example I saw when helping a beginner: The 
ncurses "menus" library. So difficult to use correctly even the official 
documentation leaks memory in its examples.

* When writing, not optimistically batching error checks over many writes. 
This is the case for .err. Do a bunch of operations, then check the error 
bit to see if any failed. "FILE *" isn't so great at this — doesn't no-op 
when the error bit is set — so just don't use it when it's not a good fit.

> where pretty much everybody sane uses some kind of {.Data, .Size}

Right! Far too late now, but I wish it had been standardized early so we'd 
all be speaking a common data/size representation.
Reply to thread Export thread (mbox)