Fall Through Error Handling in C
C error handling can be a pain, especially for multistep processes, where you need to make sure each step succeeds, and if one fails, reverse each of the preceding steps. This is the most common use of goto
s in C programming today, but the goto
command is rarely taught in college C courses anymore, and there are a few companies that will fire you no-questions-asked for using a goto
even once. Without goto
s, how should these cases be handled? Is there even a structured way to handle them that doesn’t result in unreadable code? Is there a more efficient way of doing it than using jump instructions with hardcoded addresses?1
The answer is yes, there is. It’s more structured than goto
s and more readable than deeply nested conditionals. It’s not guaranteed to be more efficient, but it should be under certain circumstances. However, this pattern may confuse others reading your code, if they are not familiar with this strategy. As such, let me strongly recommend clearly commenting the code, so that someone (maybe even yourself) who reads it in the future can easily understand it without having to work it out for themselves.
The problem with this kind of error handling is that you have a series of steps that need to be taken, and if one fails, you need to bail out and undo the previous steps, often in reverse order. You could use deeply nested conditionals, but those are hard to read and very messy when you have more than a few steps. You could use a variable to keep track of whether or not an error has occurred, and then put every subsequent step in an if statement that skips that step if the variable is set. This is also a bit messy, and it will generate bloated, slow machine code (repeated jumps, rather than jumps straight to the end). What we really need is for an error to immediately send execution to the end of the initialization, ideally to a location that will deinitialize in reverse order what has already been done, starting from the step right before the error. If it gets to the end with no error, this deinitialization code must be skipped.
So, in C, how do we skip to the end of a thing? It depends on the thing. A return will skip to the end of a function, but it won’t leave any room for deinitialization. When I first attempted this, I assumed that break
would break out of any brace delimited block of code within a function, but it turns out that is not true. It can break out of switch
statements and loops, but not out of floating blocks or conditionals. A switch
statement doesn’t make sense for initialization, as it requires an input value intended for flow control. A for
loop also carries baggage that doesn’t make sense. A while
loop actually gets very close. Set it to true, break at the end so that it never iterates more than once, and now you have the block structure you need that can be broken out of at an arbitrary location and will always break to the same location. But, there’s a better option even more well suited to this.
The solution is the do/while
loop. This kind of loop is designed specifically to always run a minimum of once. If you set the end conditional to false (or 0), it will run exactly once, with no need to break at the end to prevent it from looping. As with the other loops, break
jumps out of the loop to the next line of code right after the end of the loop, and it can be called anywhere within the loop. This gives the desired breakout behavior as cleanly as possible. It would be far better if the C standard required break
to work in floating blocks within functions, but it doesn’t, and do/while
provides an equivalent with minimal extra cruft. You should definitely leave comments explaining what you are doing though, as this is a very non-standard use of loops.
int error;
do {
// Do thing0
if (thing0_failed) {
error = 5;
break;
}
// Do thing1
if (thing1_failed) {
error = 4;
break;
}
// Do thing2
if (thing2_failed) {
error = 3;
break;
}
.
.
.
error = 99;
} while(0);
This covers the first part, the error break out. What about the reverse deinitialization? We can do that with a variable that tracks position (the error
variable above) and a switch
. Odds are you’ve never used the fall through capability of switch statements (or at least, everyone I hear whining about it says they’ve never used it and can’t imagine even one use case for it). This is where it is incredibly valuable. We can write the switch
statement with all of the deinitialization in the reverse order of the initialization statements, and then we can add case
statements to allow us to drop into the deinitialization code at the exact position we need. Without break
statements, it will just drop through doing exactly what we need. This is why switch statements need to keep this drop through behavior. If it was removed, there would be no other way to do this, aside from a massive number of conditionals containing a bunch of inefficient goto
s, and the machine code produced would be absolutely awful.
switch (er) {
case 1:
// Undo thing1
case 2:
case 3:
// Undo thing3
case 4:
// Undo thing4
case 5:
// Undo thing5
default:
break;
}
Combine these two code snippets, and now we’ve got a construct that will go through initialization step-by-step, and if any step fails, it will set an error code, break out of the initialization block, and use a switch with fall through to deinitialize anything that needs it. If a particular step does not require deinitialization, just leave that case
statement empty in the switch
. This will make it easy to add something in the future, if necessary, and it won’t do any hard in the mean time. If the initialization is successful, we set error
to a value larger than the largest error value and allow the default
case to handle it.2
The above examples are very abstract. What would this look like in real-life code? Well, the reason I came up with this pattern was a specific need, so I actually have some real life code that uses it (and quite successfully, I might add).
// Connect to the server and return a connection object
//
// hostname may be a domain name or an IP address
// port should be a port number written as a string
connection net_connect(char* hostname, char* port) {
connection conn;
X509 *cert;
do {
conn.web = BIO_new_ssl_connect(ssl_context);
if (!conn.web) {
conn.err = 8;
break;
}
BIO_set_conn_hostname(conn.web, hostname);
BIO_set_conn_port(conn.web, port);
BIO_get_ssl(conn.web, &(conn.ssl));
if (!conn.ssl) {
conn.err = 7;
break;
}
if (!SSL_set_tlsext_host_name(conn.ssl, hostname)) {
conn.err = 6;
break;
}
X509_VERIFY_PARAM_set_hostflags(SSL_get0_param(conn.ssl), X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
if (!X509_VERIFY_PARAM_set1_host(SSL_get0_param(conn.ssl),
hostname, 0)) {
conn.err = 5;
break;
}
if (BIO_do_connect(conn.web) < 1) {
conn.err = 4;
break;
}
if (BIO_do_handshake(conn.web) < 1) {
conn.err = 3;
break;
}
cert = SSL_get_peer_certificate(conn.ssl);
// Make sure a certificate was sent
if (cert) {
X509_free(cert);
} else {
conn.err = 2;
break;
}
// Make sure the certificate was valid
if (!(SSL_get_verify_result(conn.ssl) == X509_V_OK)) {
conn.err = 1;
break;
}
return conn;
} while (0);
switch (conn.err) {
case 1:
// Android specific logging
// This should probably use a #ifdef for each supported platform
__android_log_print(ANDROID_LOG_DEBUG, "TEST_APP", "%s\n", X509_verify_cert_error_string(SSL_get_verify_result(conn.ssl)));
case 2:
X509_free(cert);
case 3:
case 4:
case 5:
case 6:
case 7:
BIO_free_all(conn.web);
case 8:
break;
}
conn.web = NULL;
conn.ssl = NULL;
net_log();
return conn;
}
This is part of an HTTPS module I wrote using OpenSSL, originally intended for a (almost) pure C Android application. OpenSSL, it turns out, is quite a mess when it comes to initializing all of the various bits and pieces necessary to create an HTTPS connection. This function does that. You’ll notice some additional elements not present in the abstract examples. First, this actually returns at the end of the do/while
loop. In this case, nothing more is needed if the initialization gets that far, so we just return there, as the rest of the code in the function is for error cleanup. If we wanted to go past, we might make a 9th case in the switch
(that does nothing more than break
) and set conn.err
to 9 on success instead of returning. We would have to put the three lines following the switch
into case
8 if we did this, as they are only for error cleanup. If we did that, then we could put any finalization code that needs to run regardless of success after the switch
and before the return
.
Perhaps the best part is that my OpenSSL code above isn’t that hard to read, even if you don’t initially know what it’s doing. The while (0)
might be confusing initially, but once you realize the break
s are dropping out of the loop block early, it gets a bit more obvious. I should probably still drop a comment at the beginning of the loop explaining what it is doing though.
Overall, this is a more elegant and structured way of handling initialization sequences like this. Not only is it more readable than gotos, once you understand what it is doing, it is very likely that the compiler will be able to optimize the switch
block much more easily than a series of labels with gotos pointed at them.3 The speed of initialization code is rarely important, but with the other benefits, it’s just icing on the cake. Perhaps the greatest value, however, comes from the switch
statement providing discrete locations where it is easy to add future deinitialization code, in case something changes. Cases 3 through 6 are currently empty and just drop through to 7, but if one of those suddenly needed deinitialization at some point in the future, it would be trivially easy to just add that to the appropriate case, and it wouldn’t risk upsetting anything else. Of course, this also avoids the need to make up names for a bunch of labels to point goto
s at.
There may also be other use cases for this pattern. C doesn’t provide an official means for dropping out of a code block like this, but this is still a useful pattern. Some higher level languages use try/except or try/catch blocks to provide this functionality, and these are used a lot in those languages, so it’s clear that it has value. Just like in those languages, the most valuable use of this is probably error handling. While we could use goto
s or nested if
s, those are generally less readable and maintainable than this pattern, making this especially valuable for those cases. This is an elegant way to misuse a language feature to get highly desirable behavior that is not provided explicitly by the language.
If you aren’t familiar with assembly, you may not be aware of this, but goto
s exist as a direct way of generating an assembly jump instruction that immediately moves execution to a hardcoded address in your executable code. These addresses can be fairly small values relative to the position of the jump instruction (or the one after it, depending on the platform), or they can be full length pointers to absolute positions in memory. This means that on a 64-bit system, each jump can be up to 8 bytes plus the instruction itself, which is slower for the CPU to read in and takes more program memory than a relative jump that only takes one or two bytes. Breaking out of a loop may allow the compiler to use a relative jump with a smaller size instead of an absolute jump or a larger relative jump, saving significant bytes for initialization processes with many steps. This is not guaranteed though, so if size or speed are critical, you should try both strategies, compare the assembly code, and use whichever meets your need best.
It would actually produce better assembly for the switch
if we used an error
value one higher than the highest real error value and use an explicit case for that value. That preserves the ability of the compiler to mathematically mutate the condition variable into a memory address for every case, avoiding the extra code for checking if the address is in range and executing a default condition if it isn’t. The downside with this is that if we add an extra step, we have to remember to adjust this value both at the end of the loop and in the final case of the switch
statement.
Again, if you aren’t familiar with assembly, you may not be aware of the optimizations available for switch
statements. The default is to find a way of algebraically manipulating the condition value into a memory address that lines up neatly with the appropriate cases. This works best when you have a complete series of integers with no gaps and no default case
, but it also requires the code in each case
to be of similar length (and the compiler will pad out the difference with nop
instructions to make them exactly equal length if they aren’t already). Our particular use case is well suited to this, though the argument setup for the function call in case
1 might cause some issues here. If this doesn’t work, the compiler may create a list of pointers, where each pointer goes to the appropriate location in the code for the case it represents, and the condition variable is used to calculate the position in the list of pointers. Then it jumps to the pointer location. If the case
s aren’t continuous and in order, the switch
may have to be compiled more like a series of if
statements. For our use, we are aiming for direct calculation if possible, as that is most optimal and will generally be better than goto
s, or indirect using a pointer list, which in most cases will be similar to using goto
s and in a few potentially worse. Since we get to decide the error values, we can fine tune this toward the best outcome.