To handle or not to handle
Thoughts on error handling in C programming
When programming, we always think about what can fail, in fact, much of our program is usually handling what can go wrong and the edge cases. When starting to program in C, I used to handle every error condition. The problem with this is that, in theory, almost every system or standard library call can fail, e.g. malloc, [f]open, [f]close, etc. I believe however, that much of the error handling can be greately simplified and removed with some thought and critical thinking. Let's look at this code snippet:
bool
load_config(const char *path, struct Config *cfg)
{
... setup config
FILE *f = fopen(path, "r");
if (!f) {
... free config, return failure
}
... load config
if (!load_part_of_config(cfg, ...)) {
... close file, free config, return failure
}
... load config
if (fclose(f) == EOF) {
... free config, return failure
}
... return success
}
bool
load(const char *path, struct Data *data)
{
struct Config cfg;
if (!load_config(path, cfg)) {
... return error
}
...
... return success
}
int
main(int argc, char **argv)
{
char *path;
struct Data data = {0};
...
if (!load(path, &data)) {
die("load failed");
}
...
return 0;
}
In the `load_config` function we can see that we are first allocating the config structure at the start, then opening the config file, doing some loading from the file, some parts which might fail and then closing the file. There are three points of error handling here, and we may add one more if we take into account the closing of the file in the second `if`. The remaining functions are just to illustrate the point of this article and are self-explanatory.
This code may look `clean` and `safe`, since it handles all edge cases. Therefore one might say that it is adequate. Now come some observations on how I might improve this code.
Some functions should not fail
Look at the `fclose` function. Ask yourself one question: when will `fclose` fail on an already opened file? If `fclose` can't fail, then we could just ignore the handling of the error. However, we expect the function to not fail. If it fails, something has gone terribly wrong in our program and our view of the state of the program is not the computers view (too bad). I prefer to change the it for an assert: `assert(fclose(f) == 0)`. Therefore, when I make a mistake and something does not go as planned, there is a chance to capture the problem early. Furthermore, if you have setup the dumping of a corefile upon abort, you can just plug in the debugger and see where the program aborted and the cause of the problem.
Functions whose fail terminate the program
It is often the case that when the function fails, it leads to a cascade of error returns until it reaches the top of the call tree and the program terminates. In this case, this function can be greately simplified by terminating at this point, which saves us from having to setup the error uplifting chain. We can then change the return type to void so that the parent function can assume that when the function returns, it did it successfully, removing all the overhead associated with the error handling. There is a caveat to this approach, however. If the function gets called from multiple places, we may output a error message that lacks the context from which it was called, leaving us wondering from where did the function get called. For this I have no great answer, this approach works best on one-time functions that are called predictably.
Final result
void
load_config(const char *path, struct Config *cfg)
{
... setup config
FILE *f = fopen(path, "r");
if (!f)
die("failed to open `%s` file", path);
... load config
load_part_of_config(cfg, ...));
... load config
assert(fclose(f) == 0);
}
void
load(const char *path, struct Data *data)
{
struct Config cfg;
load_config(path, cfg);
...
}
int
main(int argc, char **argv)
{
char *path;
struct Data data = {0};
...
load(path, &data);
...
return 0;
}
Simplifying error handling
Specially in a library environment, in which errors usually are to be returned, one can use `gotos` to simplify error handling. This strategy can be applied when [de]allocation of resources may/can be done in reverse order. This is common knowledge, but I leave it here in case someone finds this for the first time:
bool
load_config(const char *path, struct Config *cfg)
{
... setup config
FILE *f = fopen(path, "r");
if (!f)
goto error_file;
... load config
if (!load_part_of_config(cfg, ...))
goto error_part;
... load config
assert(fclose(f) == 0);
... return success
error_part:
assert(fclose(f) == 0);
error_file:
... free config
... return failure
}
NOTE: this approach has the added benefit to scare undesired people out of your code
Arbitrary thoughts
Code size is not important, readability is what matters
Don't duplicate too much code, nor non-duplicate too much code, extremes are bad
Izan, 28 December 2025, LLU Blog