Andrew Kesterson

"Love God. Live Righteously. Die Well."
GitHub || LinkedIn
21 Posts 10 Categories 0 Tags
Categories
Books 13
Faith 6
Outdoors 1
Technology 3
Leadership 7
Philosophy 1
technology 1
Current-Events 1
History 1
Liberal-Education 1
hot tags

# libakerror

2026-01-11 technology 5.6k words

I’d like to present a C library I wrote for exception style error handling in C code, why I wrote it, and why I prefer this style of error handling when writing in the C language.

libakerror: A safe exception handling library for C

To be clear, when I say “exception style error handling”, I am referring to the practice of one section of code creating an `exception` object, which is an object containing a rich error context, and then the language `raising` (or `throwing`) that exception up through the call stack, allowing all code paths in the call stack the opportunity to respond to that exception, and if the exception reaches all the way back up to the program’s top level without being handled, it will terminate the program.

Before completing this article, I strongly recommend that you go and read the libakerror README, at least the “Library Architecture” section. This article will not spend a lot of time explaining what libakerror does, or even how it does it, but rather why I prefer this style of error handling. So if you don’t go through the README, you may be a bit lost for the rest of the article.

Note that the question of “why do you like writing software in C so much” won’t be answered here. That’s a separate blog post TBD. This post also assumes you are already familiar with the C programming language, although if you know any programming languages at all, you can probably get through it. If you don’t know C, I strongly recommend these two books:

- The C Programming Language by Kernigan & Ritchie aka “the K&R C book”
- Modern C by Jens Gustedt

## Getting roasted in IRC is motivational

I recently decided to do a fun thing: I took this software library on LiberaChat to #c and asked them roast it. Fun times!

It did result in some improvements to the library (what doesn’t kill us makes us stronger), but mostly what it resulted in was illustrating a huge philosophical divide between me and some other C programmers. Essentially it can be summed up as

    <#c user> this is solving a problem that doesn't exist

    <#c user> and you're a retard

Touché, sir.

To be fair, the jury is still (and will probably remain) out on the extent and nature of my particular retardation. I’m open to the possibility. But let’s address the more serious question.

## There is no problem. C is not broken.

Let me be clear: There is nothing wrong with the C language. I hold that C is basically perfect (fight me). This is especially true about its error handling mechanisms: C’s error handling is not broken, and this library does not claim to fix it.

Well if it’s perfect then why write the library?

## Because we can do better

Even though it’s not broken, I think it provides us the tools to do better. Consider this code:

    #include <stdio.h>

    int main(void)
    {
    	FILE *fp = fopen("somefile.txt", "w");
    	if ( fp == NULL ) {
    		return 1;
    	}
    	return fprintf(fp, "Some message\n");
    }

What is the return value of this function? … In the simplest case, it is equal to `strlen("Some message\n")`. But what about when it fails? man describes the failure behavior of `fprintf` thusly:

    If an output error is encountered, a negative value is returned.

So …. there are conceivably times when, theoretically, we could get a valid file pointer, pass it off to fprintf(), and get a non-zero return code. We can capture that error code, we can react to it, but we are left to wonder about the meaning of that return code.

If we look at the libc source code for fprintf, we see that there are several possible failure modes:

- arguments malformed (NULL file pointer or message)
- streams are incorrectly oriented (character or wide character)
- errors on IO locking and/or unlocking
- errors flushing buffers to disk (such as no space remaining on the output device)

So, yeah, we know that there was a failure. Something bad happened. But what? And why?

## Check errno, stupid

In the case of fprintf and other well behaved libc functions, the C standard requires that they set `errno`. This is a global variable that contains an integer value corresponding to an error code. You can see which values of `errno` are valid on your system at the command line:

    $ errno --list
    EPERM 1 Operation not permitted
    ENOENT 2 No such file or directory
    ESRCH 3 No such process
    EINTR 4 Interrupted system call
    EIO 5 Input/output error
    ENXIO 6 No such device or address
    E2BIG 7 Argument list too long
    ENOEXEC 8 Exec format error
    EBADF 9 Bad file descriptor
    ECHILD 10 No child processes
    ENAMETOOLONG 36 File name too long
    .....

So in the simple case above, we can get the exact reason from `errno`. Then we can react with a much more specific course of action, even if the correct action is, “abort the program”. Consider:

    int rc = fprintf(fp, "Some message\n");
    if ( rc < 0 ) {
    	printf("Failed to write");
    	switch ( errno ) {
    	case ESTALE:
            // Stale file handle
    		// do something about it here and continue (somehow)
    		break;
    	case ENOSPC:
    	    // No space on the device
    		// do something about it here and continue (somehow)
    		break;
    	default:
    		// Some other undefined error, all we know how to do is abort
    		return 1;
    	}
    }

This isn’t bad, but it is a little bit inconvenient.

- You have to know that the library in question returns one value, but that value is not the actual error, the actual error is somewhere else
- You have to fetch the error from somewhere else and check it explicitly
- You have to explicitly handle every possible error case and ensure you have a default error handler.

## The libakerror way

How does that same fprintf checking code look in `libakerror`?

    #include <akerror.h>
    #include <errno.h>
    #include <string.h>

    int main(void) {
        FILE *fp = fopen("somefile.txt", "w");
        PREPARE_ERROR(errctx);
        ATTEMPT {
    	FAIL_NONZERO_BREAK(errctx, fprintf(fp, "Some message\n"), errno, "Failed to write");
        } CLEANUP {
        } PROCESS(errctx) {
        } HANDLE(errctx, ESTALE) {
    	    // Stale file handle
    	    // do something about it here and continue (somehow)
        } HANDLE(errctx, ENOSPC) {
    	    // No space on the device
    	    // do something about it here and continue (somehow)
        } HANDLE_DEFAULT(errctx) {
    	    // Some other undefined error, all we know how to do is abort
    	    return 1;
        } FINISH_NORETURN(errctx);
    }

I believe this approach has several advantages over the previous method, without even digging into some of the library’s cooler features:

- It’s 2 lines shorter (not much, but it counts)
- The separate portions of the error checking logic are placed into explicitly named scopes (attempting something, cleaning up from the attempt, and handling errors), clarifying what each block of code is doing
- The initial process of performing an action, capturing the initial return code, and checking further `errno` details, and storing it into a single error context object, is done on one line that also encodes your intent (“fail if this operation returns non-zero”)
- There is an explicit cleanup step in the error handling process - more on that in a minute
- The default error handling behavior is wrapped into the FINISH macro; we don’t have to worry about it

## But the macros hide a lot of complexity

Fair enough. Let’s compare what the two look like after the preprocessor is done with them.

First, our test case without libakerror:

       int rc = fprintf(fp, "Some message\n");
       if ( rc < 0 ) {
    printf("Failed to write");
    switch ( errno ) {
    case ESTALE:

        break;
    case ENOSPC:

        break;
    default:

        return 1;
    }
       }

As expected, it looks basically exactly as we wrote it, whitespace not withstanding.

Now here is the preprocessed libakerror code (after cleaning it up - the preprocessor has no respect for humans and how we see whitespace):

    // PREPARE_ERROR(errctx);
    akerr_error_init();
    akerr_ErrorContext __attribute__ ((unused)) *errctx = ((void *)0);;

    // ATTEMPT
    switch ( 0 ) {
    case 0: {
        // FAIL_NONZERO_BREAK
        if ( fprintf(fp, "Some message\n") != 0 ) {
    		if ( errctx == ((void *)0)) {
    			errctx = array_next_error();
    			if ( errctx == ((void *)0)) {
    				error_log_method("%s:%s:%d: Unable to pull an ErrorContext from the array!", "test2.c", (char *)__func__, 13);
    				exit(1);
    			}
    		}
    		errctx->refcount += 1;;
    		errctx->status = errno;
    		snprintf((char *)errctx->fname, 256, "test2.c");
    		snprintf((char *)errctx->function, 128, __func__);
    		errctx->lineno = 13;
    		snprintf((char *)errctx->message, 1024, "Failed to write");
    		errctx->stacktracebufptr += snprintf(errctx->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%s:%s:%d: %d (%s) : %s\n", (char *)errctx->fname, (char *)errctx->function, errctx->lineno, errctx->status, akerr_name_for_status(errctx->status, ((void *)0)), errctx->message);;
    		break;
    		};
        }
    };

    // CLEANUP { }
    {
    }

    // PROCESS {
    if ( errctx != ((void *)0) ) {
        switch ( errctx->status ) {
        case 0:
    	errctx->handled = true;
    	{}
    	break;
        // HANDLE(errctx, ESTALE)
        case ESTALE:
    	errctx->stacktracebufptr = (char *)&errctx->stacktracebuf;
    	errctx->handled = true;
    	{}
    	break;
        // HANDLE(errctx, ENOSPC)
        case ENOSPC:
    	errctx->stacktracebufptr = (char *)&errctx->stacktracebuf;
    	errctx->handled = true;
    	{}
    	break;
        // HANDLE_DEFAULT(errctx)
        default:
    	errctx->stacktracebufptr = (char *)&errctx->stacktracebuf;
    	errctx->handled = true;
    	{
    	    return 1;
    	}
        };
    };

    // FINISH_NORETURN(errctx)
    if (errctx != ((void *) 0)) {
        if (errctx->handled == 0) {
            akerr_log_method ("%s%s:%s:%d: %s %d (%s): %s", (char *) &errctx->stacktracebuf, (char *) "test2.c", (char *) __func__, 21, "Unhandled Error", errctx->status, akerr_name_for_status (errctx->status, ((void *) 0)), errctx->message);;
            akerr_handler_unhandled_error (errctx);
        }
    }
    if (errctx != ((void *) 0))
    {
        errctx = akerr_release_error (errctx);
    };;

The preprocessed libakerror code, prettied up, expands to 72 lines. That’s quite a balloon from our original 15 lines of error handling code.

## We don’t like it

There are a few obvious criticisms we can level against the preprocessed `libakerror` code here, and I think all of them are defensible.

- It’s switch statements all the way down, man. Yes, the macros primarily just wrap a big switch statement that detects when the error has been set and uses case statements to define how to handle it. As I said, C isn’t broken. And if it’s not broke, don’t fix it. We just wrap it.
- It’s calling methods I didn’t ask for, like `error_init()` Yes. There are a few helper methods that are called to manage the internal error state of the library as part of the macros. The things it’s doing are:
  - Initializing the library and its objects, if they are not already initialized (one-time cost)
  - Finding the next available error context object (or failing gracefully if there is not one available). The performance of this method is predictable related to the `MAX_HEAP_ERROR` setting defining the maximum number of error frames, and it doesn’t do much. The other less performant code (like managing error context objects, string formatting, etc) are never executed unless there is an error detected.
- That FAIL_NONZERO_BREAK macro is freakin huge. Yes, there’s a lot of code hidden behind the `CATCH` and `FAIL_*` macros. But if you look at what they’re doing, it’s setting up a durable error context object with a ton of context about the error and where it occurred. This (to me) is worth the lines of code.
- It’s doing expensive string formatting. This is true but only in the cases where an error has been detected. None of that code is executed when there is no error.
- You’re doing things with pointers! That’s dangerous! This is a C library, get over it. Pointer management is not inherently unsafe behavior, and what we’re doing here is not particularly creative or risky.

## Why I like it

Because it helps me in several ways:

1. It helps me avoid situations where I don’t check errors
2. It helps me debug a program error when all I have is a log and can’t attach a debugger
3. It helps me ensure cleanup behavior occurs on errors
4. It creates an automatic “jump to an error handler on error” pattern without using `goto`

Again, C is not broken, and you can do all of this without this library. This library just makes it easier. And as humans, if something is easier, we are more likely to do it.

### Ensuring errors get checked

It’s easy to end up in a situation where you don’t check a return code.

    char *memptr;
    memptr = malloc(SOME_SIZE); /* Uh oh, what happens if memory allocation fails? */
    strcpy(memptr, "Some string"); /* Kaboom! */

This exact behavior is the source of a stunning number of errors in professional code that runs large portions of very important systems. Obviously you can discipline yourself to check and handle errors

    char *memptr;
    memptr = malloc(SOME_SIZE);
    if ( memptr == NULL ) {
        return 1; /* Let's assume our caller knows what to do */
    }
    strcpy(memptr, "Some string");

This is how the grand majority of C code ever written solves these problems. Modern C standards add a helpful keyword to cause our compilers to emit an error when return codes are not checked. For example in gcc you can:

    extern int dangerous_func(void) __attribute__((warn_unused_result));

    int main(void) {
        dangerous_func(); // Compiler will warn here
        int rc = dangerous_func(); // OK, return value is used
        return 0;
    }

Unfortunately it’s not so much a language standard so compilers may implement it differently. For example MSVC uses a `_Check_return` annotation instead. But they both offer the functionality.

This also only helps if the libraries you’re calling (like `malloc`) were defined with this semantic. And, again unfortunately, the C standard library does not require (or embrace) this mechanism. C stdlib’s default philosophy (which I generally agree with) is “I assume you know what you’re doing”. Unfortunately, assuming I know what I’m doing (or that any human knows what they’re doing) is a famously dangerous thing.

So if we can’t rely on our standard library enforcing that we’re doing something with return codes, we have to make it easier to discipline ourselves into checking the code. We have to build habits. Checking for a null pointer and returning 1 does not seem like a difficult habit to build. And it certainly seems simple to build when compared to stacking a series of macros.

**Naked C** | **libakerr C**
    char *memptr;
    memptr = malloc(SOME_SIZE);
    if ( memptr == NULL ) {
    	printf("Failed to allocate memory");
        return 1;
    }
    strcpy(memptr, "Some string");
    PREPARE_ERROR(errctx);
    ATTEMPT {
      char *memptr;
      FAIL_ZERO_BREAK(errctx, (memptr = malloc(SOME_SIZE)), AKERR_NULLPOINTER, "Failed to allocate memory");
      strcpy(memptr, "Some string");
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);

Again, it’s only 1 line longer than the naked C. The working line in libakerr seems longer - however the `FAIL_ZERO_BREAK` line is only 102 characters, while the naked C code (memptr assignment from malloc, result comparison, and return statement) is 150 characters. Even I’m surprised, because honestly looking at the libakerror code, it does feel longer and more verbose to me. It’s probably some headtrick with the length of that `FAIL_ZERO_BREAK` line.

Now, that doesn’t matter, because we all know that the keyboard is not the choke point on getting code written. But it’s worth pointing out - if something feels like it’s taking longer, then the programmmer will hate it. And that’s where we come to another philosophical difference: The naked C version feels like it’s slower to write. (Because it actually is!) This means I will be less likely to discipline myself when writing the Naked C code, and more likely to take shortcuts, because (as much as I love writing code and I love writing it in C) ultimately I want to get it done and make it work.

Ultimately, by allowing me to write my code in such a way that error checking feels less cumbersome, I am confident that libakerror helps me solve a very major problem that most of us tend to overlook:

### Debugging when all you have is logs

> Without good debug logs, a program is much more difficult to debug.

I’ve spent the majority of my career working with software that runs in environments where the only hope for debugging it is to interrogate some log files after an error has already occurred. Getting the chance to attach a debugger and examine the state of the running program is a rare treat that we almost never get to experience in the DevSecOps world. So if a program does not emit useful logs, in my opinion, it is basically impossible to debug it in my world.

And even when you have a debugger, a log is still a good way to quickly hone in on where something happened, even if you must take a much longer time to figure out why it happened. Let’s consider the code in one of the libakerror test programs.

In this code, we have a call graph like `main() -> func1() -> func2()`. In `func2()` we experience some error that causes us to exit early. If we were to run this in naked C, it might look like this:

    #include <stdio.h>

    int func2(void)
    {
    	printf("This is a failure in func2");
    	return 1;
    }

    int func1(void)
    {
    	return func2();
    }

    int main(void)
    {
    	return func1();
    }

Now running the naked C code, we get

    $ ./test ; echo $?
    This is a failure in func2
    1

This might seem like a good result. It tells us that there was an error, and where that error occurred. This is easy to do by manually adding debug printfs in naked C. But there is a question it does not help us answer, a question that is incredibly important in the real world:

> HOW DID WE GET TO WHERE THE ERROR HAPPENED IN THE FIRST PLACE?

In this contrived example we know clearly that `func1()` called `func2()` and so here we are. But let’s say we’re working with some amount of code that is a black box to us - maybe our code is a callback from another library and we can’t be sure just exactly what the code path is that’s being taken. Or maybe it’s our own code, and we’ve simply forgotten all the possible pathways that this code could be reached. (This happens far more often than we want to admit.)

Now if we look at the libakerror version:

    #include "akerror.h"

    akerr_ErrorContext *func2(void)
    {
        PREPARE_ERROR(errctx);
        ATTEMPT {
    	    FAIL(errctx, AKERR_NULLPOINTER, "This is a failure in func2");
        } CLEANUP {
        } PROCESS(errctx) {
        } FINISH(errctx, true);
        SUCCEED_RETURN(errctx);
    }

    akerr_ErrorContext *func1(void)
    {
        PREPARE_ERROR(errctx);
        ATTEMPT {
    	    CATCH(errctx, func2());
        } CLEANUP {
        } PROCESS(errctx) {
        } FINISH(errctx, true);
        SUCCEED_RETURN(errctx);
    }

    int main(void)
    {
        PREPARE_ERROR(errctx);
        ATTEMPT {
        	CATCH(errctx, func1());
        } CLEANUP {
        } PROCESS(errctx) {
        } FINISH_NORETURN(errctx);
    }

… and the libakerror output:

    libakerror/tests/err_trace.c:func2:7: 1 (Null Pointer Error) : This is a failure in func2
    libakerror/tests/err_trace.c:func2:10
    libakerror/tests/err_trace.c:func1:18: Detected error 0 from array (refcount 1)
    libakerror/tests/err_trace.c:func1:18
    libakerror/tests/err_trace.c:func1:21
    libakerror/tests/err_trace.c:main:30: Detected error 0 from array (refcount 1)
    libakerror/tests/err_trace.c:main:30
    libakerror/tests/err_trace.c:main:33: Unhandled Error 1 (Null Pointer Error): This is a failure in func2

Every `akerr_ErrorContext` object that’s been populated with `PREPARE_ERROR()` contains the file, line number, and function name of the location where it is when the error is encountered. The stack frame that actually detects the error includes the actual error code, a description of the error code’s meaning, and a short message from the `CATCH` or `FAIL_*` macro that set it. This gives us a pretty good stacktrace to start diagnosing the problem.

Now there are other ways of getting a stacktrace, and they are arguably better options than what’s being done here, but they have a few caveats:

1. Lots of the options only work well on Linux or BSD variants
2. Some of the options only work well with a certain binary format (such as ELF)
3. Since all of them inspect the call stack at runtime, they can’t tell you things like file names, line numbers, and method names unless the code was compiled with DWARF or other debugging symbols enabled

The libakerror backtrace code is limited in that it can only tell you about the frames wherein `PREPARE_ERROR()`, `CATCH` or `FAIL_*` macros are used, but:

1. The behavior is entirely cross platform. It works equally well on any platform where your code will compile that supplies `__FILE__`, `__LINE__` and `__func__` macros. (Every compiler supplies these as far as I know.)
2. The behavior works independently of machine architecture or binary formats
3. Because these are all preprocessor macros, the names of source files, lines, and function names are inserted at compile time, requiring no debugging symbols to be explicitly included in your binary.

And I think that’s a perfectly acceptable set of tradeoffs.

### Ensuring cleanup behavior occurs on error

This is another very common class of problems in basically every language, not just C. Let’s consider this code:

    int writeToFile(char *fname, char *string)
    {
    	int success = 0;
    	FILE *fp = fopen(fname, "w");
    	if ( fp == NULL ) {
    		printf("Error %d (%s)", errno, strerror(errno));
    	    return 1;
    	}
       	if ( string == NULL ) {
       		printf("Can't print null string to file!\n");
       		return 1;
       	}
    	success = strlen(string);
       	if ( fwrite(fp, string) != success ) {
    		printf("Error %d (%s)", errno, strerror(errno));
    		return 1;
    	}
    	if ( fclose(fp) != 0 ) {
    		printf("Error %d (%s)", errno, sterror(errno));
    		return 1;
    	}
    	return 0;
    }

The error should stand out. When we check for the null string pointer, we return early, but we forget to close the file handle we just opened. Using `libakerror` helps prevent this behavior in 2 ways:

1. Since I have a macro that inserts a `PREPARE .. ATTEMPT .. CLEANUP .. PROCESS .. HANDLE .. FINISH` block at the stroke of a single key, I’m automatically presented with a clearly named block where I’m supposed to put cleanup code. This triggers me to ask myself “what should I clean up here?”, and write that code. (To be fair, you can do the same thing with a macro and a naked C block with a comment inside of it that prompts you appropriately.)
2. If I begin a `ATTEMPT` block and omit the `CLEANUP` block, the macros generate invalid code, and compilation will fail. (Granted - the compilation fails in a way that is confusing in the way that a missing brace is confusing, but it does prevent running code with an incomplete error management block. Not quite rust, but I’ll take it.)

In libakerror, this code becomes:

    akerr_ErrorContext *writeToFile(char *fname, char *string)
    {
    	PREPARE_ERROR(errctx);
    	FILE *fp = NULL;
    	int success = 0;
    	ATTEMPT {
    	    FAIL_ZERO_BREAK(errctx, (fp = fopen(fname, "w")), errno, "Failed to open %s : %s", fname, strerr(errno));
    		FAIL_ZERO_BREAK(errctx, string, AKERR_NULLPOINTER, "Null pointer");
    		success = strlen(string);
       		FAIL_NONZERO_BREAK(errctx, (fwrite(fp, string) == success), errno, strerror(errno));
    	} CLEANUP {
    		FAIL_NONZERO_RETURN(errctx, fclose(fp), errno, strerror(errno));
    	} PROCESS(errctx) {
    	} FINISH(errctx, true);
    	SUCCEED(errctx);
    }

Now this is, of course, a contrived example made purely to prove a point - this could just as easily be solved by moving the string `NULL` check above the opening of the file pointer (which is, in fact, the more correct thing to do). But this issue (needing to perform some kind of clean up, such as freeing memory or closing files, after an error) is very common in the real world and this was an easy way to demonstrate it.

### Managing early exit handlers with cleanup without goto

Continuing the `writeToFile()` example from above, we have an issue of best practice. As the code is currently written (in naked C), when we fix the code to add the cleanup after the string check, we wind up with this:

    int writeToFile(char *fname, char *string)
    {
    	int success = 0;
    	FILE *fp = fopen(fname, "w");
    	if ( fp == NULL ) {
    		printf("Error %d (%s)", errno, strerror(errno));
    	    return 1;
    	}
       	if ( string == NULL ) {
       		printf("Can't print null string to file!\n");
    		fclose(fp);
       		return 1;
       	}
    	success = strlen(string);
       	if ( fwrite(fp, string) != success ) {
    		printf("Error %d (%s)", errno, strerror(errno));
    		return 1;
    	}
    	if ( fclose(fp) != 0 ) {
    		printf("Error %d (%s)", errno, sterror(errno));
    		return 1;
    	}
    	return 0;
    }

… But wait a second … we know that it’s possible (although odd) that `fclose(fp)` could ALSO fail. We might need to report that error as well! So now we do what most programmers do, and we duplicate the code.

    int writeToFile(char *fname, char *string)
    {
    	int success = 0;
    	FILE *fp = fopen(fname, "w");
    	if ( fp == NULL ) {
    		printf("Error %d (%s)", errno, strerror(errno));
    	    return 1;
    	}
       	if ( string == NULL ) {
       		printf("Can't print null string to file!\n");
    		if ( fclose(fp) != 0 ) {
    			printf("Error %d (%s)", errno, sterror(errno));
    			return 1;
    		}
       		return 1;
       	}
    	success = strlen(string);
       	if ( fwrite(fp, string) != success ) {
    		printf("Error %d (%s)", errno, strerror(errno));
    		return 1;
    	}
    	if ( fclose(fp) != 0 ) {
    		printf("Error %d (%s)", errno, sterror(errno));
    		return 1;
    	}
    	return 0;
    }

Gag me with a spoon. I hate it, and so do you. So we have another pattern that we use: the early exit handler with our friend that everyone loves to hate, goto.

    int writeToFile(char *fname, char *string)
    {
    	int success = 0;
    	int rc = 0;
    	FILE *fp = fopen(fname, "w");
    	if ( fp == NULL ) {
    		goto _writeToFile_earlyexit
    	}
       	if ( string == NULL ) {
       		printf("Can't print null string to file!\n");
       		rc = 1;
    	   	goto _writeToFile_cleanup
       	}
    	success = strlen(string);
       	if ( fwrite(fp, string) != success ) {
    		goto _writeToFile_earlyexit
    	}
    	goto _writeToFile_cleanup
    _writeToFile_earlyexit:
    	printf("Error %d (%s)", errno, sterror(errno));
    	return 1;
    _writeToFile_cleanup:
    	if ( fclose(fp) != 0 || rc != 0 ) {
    		goto _writeToFile_earlyexit
    	}
    	return 0;
    }

Despite the revulsion that I know a lot of you are feeling right now, this code is better than the code we had before, and it is in fact using a common pattern that is acceptable in the industry. However, this has several problems.

1. The use of `goto` in the C language is so universally feared that most C linters will report this code as a warning at the least, and some static code analyzers may in fact flag it as an “unsafe” risk, blocking your code from leaving the build system
2. You will feel bad about publishing it (unless you’re me, go off)
3. `goto` labels share a single universal namespace, so they have to be very lengthy, and writing a ton of them can get annoying
4. In order to do this properly, we had to introduce two separate blocks: one for the cleanup, one for the early exit behavior. This is because some code paths don’t need to call the cleanup, but all code paths (including the cleanup) might need to call the early exit behavior. So in order to prevent duplicating code, we made two `goto` targets.
5. For the same reason as above, we had to introduce a new variable, `rc`, to hold the intended final return code for our method - either `0` or `1`. Then, because the cleanup method is in the unhappy path for the string null check, and we must flow from there into the earlyexit handler, but ONLY in the case when we in the unhappy code path, we had to set the `rc` variable in the string NULL check and then check it again in the cleanup method, using it as a flag to divert execution into the early exit handler.

> “That’s crazy. Real C programmers don’t do this! This is just proof that you’re retarded!”

Again, I accept the possibility that I’m just an idiot. But I promise you, real C programmers do this with remarkable frequency. To be clear, you do NOT have to do this. There ARE better ways. Creative use of `switch`, and `break` will give you a better result.

But I don’t worry about it, because using `libakerr`, I do not have to manage this! My early exit behavior is baked into the macros and the macro structure. We don’t even need to change the `libakerror` code from our previous example to get the same (but smarter) behavior as what you see in the naked C example; it’s already doing it. Look at the cleaned up preprocessed code after the macros have been expanded, look at where the code may return, why, and how the cleanup logic is handled.

    akerr_ErrorContext *writeToFile (char *fname, char *string)
    {
    	// PREPARE_ERROR(errctx)
        akerr_init ();
        akerr_ErrorContext __attribute__((unused)) * errctx = ((void *) 0);;

        FILE *fp = ((void *) 0);
        int success = 0;

    	// ATTEMPT
        switch (0) {
        case 0: {

    		// FAIL_ZERO_BREAK(errctx, (fp = fopen(fname, "w")), errno, "Failed to open %s : %s", fname, strerror(errno));
            if ((fp = fopen (fname, "w")) == 0) {
                if (errctx == ((void *) 0)) {
                    errctx = akerr_next_error ();
                    if (errctx == ((void *) 0)) {
                        akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", "test2.c", (char *) __func__, 118);
                        exit (1);
                    }
                }
                errctx->refcount += 1;;
                errctx->status = (*__errno_location ());
                snprintf ((char *) errctx->fname, 256, "test2.c");
                snprintf ((char *) errctx->function, 128, __func__);
                errctx->lineno = 118;
                snprintf ((char *) errctx->message, 1024, "Failed to open %s : %s", fname, strerror ((*__errno_location ())));
                errctx->stacktracebufptr += snprintf (errctx->stacktracebufptr, 2048, "%s:%s:%d: %d (%s) : %s\n", (char *) errctx->fname, (char *) errctx->function, errctx->lineno, errctx->status, akerr_name_for_status (errctx->status, ((void *) 0)), errctx->message);;
                break;
    	    };

    		// FAIL_ZERO_BREAK(errctx, string, AKERR_NULLPOINTER, "Null pointer");
            if (string == 0) {
                if (errctx == ((void *) 0)) {
                    errctx = akerr_next_error ();
                    if (errctx == ((void *) 0)) {
                        akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", "test2.c", (char *) __func__, 119);
                        exit (1);
    		        }
    	        }
                errctx->refcount += 1;;
                errctx->status = 1;
                snprintf ((char *) errctx->fname, 256, "test2.c");
                snprintf ((char *) errctx->function, 128, __func__);
                errctx->lineno = 119;
                snprintf ((char *) errctx->message, 1024, "Null pointer");
                errctx->stacktracebufptr += snprintf (errctx->stacktracebufptr, 2048, "%s:%s:%d: %d (%s) : %s\n", (char *) errctx->fname, (char *) errctx->function, errctx->lineno, errctx->status, akerr_name_for_status (errctx->status, ((void *) 0)), errctx->message);;
                break;
    	    };
            success = strlen (string);

            // FAIL_NONZERO_BREAK(errctx, (fprintf(fp, "%s", string) == success), errno, "%s", strerror(errno));
            if ((fprintf (fp, "%s", string) == success) != 0) {
                if (errctx == ((void *) 0)) {
                    errctx = akerr_next_error ();
                    if (errctx == ((void *) 0)) {
                        akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", "test2.c", (char *) __func__, 121);
                        exit (1);
    		        }
    	        }
                errctx->refcount += 1;;
                errctx->status = (*__errno_location ());
                snprintf ((char *) errctx->fname, 256, "test2.c");
                snprintf ((char *) errctx->function, 128, __func__);
                errctx->lineno = 121;
                snprintf ((char *) errctx->message, 1024, "%s", strerror ((*__errno_location ())));
                errctx->stacktracebufptr += snprintf(errctx->stacktracebufptr, 2048, "%s:%s:%d: %d (%s) : %s\n", (char *) errctx->fname, (char *) errctx->function, errctx->lineno, errctx->status, akerr_name_for_status (errctx->status, ((void *) 0)), errctx->message);;
                break;
    	    };
            }
        };

    	// CLEANUP
        {
    	if (fclose (fp) != 0) {
    	    if (errctx == ((void *) 0)) {
    		    errctx = akerr_next_error ();
    		    if (errctx == ((void *) 0)) {
    		        akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", "test2.c", (char *) __func__, 123);
    		        exit (1);
    		    }
    	    }
    	    errctx->refcount += 1;;
    	    errctx->status = (*__errno_location ());
    	    snprintf ((char *) errctx->fname, 256, "test2.c");
    	    snprintf ((char *) errctx->function, 128, __func__);
    	    errctx->lineno = 123;
    	    snprintf ((char *) errctx->message, 1024, "%s", strerror ((*__errno_location ())));
    	    errctx->stacktracebufptr += snprintf(errctx->stacktracebufptr, 2048,"%s:%s:%d: %d (%s) : %s\n", (char *) errctx->fname, (char *) errctx->function, errctx->lineno, errctx->status, akerr_name_for_status (errctx->status, ((void *) 0)), errctx->message);;
    	    return errctx;
    	};
        }

    	// PROCESS(errctx)

        if (errctx != ((void *) 0)) {
    	    switch (errctx->status) {
                case 0:
                errctx->handled = 1;
    	        {
    	        }
            };
        };

    	// FINISH(errctx, true)
        if (errctx != ((void *) 0)) {
    		if (errctx->handled == 0 && 1 == 1) {
    			errctx->stacktracebufptr += snprintf (errctx->stacktracebufptr, 2048, "%s:%s:%d\n", (char *) "test2.c", (char *) __func__, 125);
    			return errctx;
    		}
        }
        if (errctx != ((void *) 0)) {
            errctx = akerr_release_error (errctx);
        };;

    	// SUCCEED_RETURN(errctx);
        if (errctx != ((void *) 0)) {
          errctx = akerr_release_error (errctx);
        };
        return ((void *) 0);;
    }

Here we can see that using the `libakerror` framework, our code:

1. Exits early whenever an `akerr_ErrorContext` object is unable to be initialized (via `exit(1)`)
2. Returns from the `CLEANUP` block if the `fclose(fp)` call fails, returning the `akerr_ErrorContext` up to the calling context
3. Returns 0 at the end of the function assuming everything happens correctly

And it does it all without `goto`.

## Solving a problem you don’t actually have

So, let’s revisit the claim

    <#c user> this is solving a problem that doesn't exist

1. We have been able to demonstrate several varieties of problems which are very real, very common, and very much present even in our beloved C language.
2. We have been able to demonstrate that `libakerror` solves these problems quite handily

## I still don’t like it

And that’s fine. It’s okay for people to like different things. But if you have the same kinds of problems I’ve described here, and you love (or want to love) writing in C, but error handling has made you gunshy … Feel free to take this code and use it. It’s free (as in speech).

Recent Posts
2026-01-11
News - 2026 - Week 1
2026-01-11
Single Pane of Glass
2026-01-11
Smart LEGO
2026-01-11
libakerror