Try Catch in C
Try-Catch
mechanisms are common in many programming languages such as Python, C++, and JavaScript. The general structure is below.
try {
/*
Insert some lines of code that will probably give you errors
*/
} catch {
/*
Write some code to handle the errors you're getting.
*/
}
They allow you to write code without having to test each statement. If the program running in the try
block reaches an exception, the exception is passed to the catch
block.
If the exception matches some exception type, the code inside the catch
block is executed. Otherwise, the exception is passed back to the try
block.
Try-Catch
in C
C does not support exception handling. At least, it does not have any built-in mechanism for it.
This guide will demonstrate a possible solution to provide try-catch
functionality in C. It should be noted that the solution is not necessarily complete.
Exception handling systems are not complete and safe without a mechanism to free up memory when the stack has been traversed, and C does not have a garbage collector. We would also probably need to include context managers to free up memory.
This solution does not intend to provide a complete and extensive try-catch
mechanism. This concept can technically be used to handle a few exceptions.
We will build the solution incrementally, making updates to the code. We will use two functions provided by C, longjmp
and setjmp
, which can be obtained from the setjmp.h
header file.
We will take a closer look at both functions’ definitions.
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
setjmp
takes a variable of type jmp_buf
. When this function is called directly, it returns 0
.
longjmp
takes two variables, and when longjmp
is invoked with the same jmp_buf
variable, the setjmp
function returns with the same value as the second argument of longjmp
(val
).
The env
variable here is essentially the calling environment, representing the state of registers and the position in the code when the function call is being made. When longjmp
is called, the state in the calling environment is copied to the processor, and the value stored in the val
argument of longjmp
is returned.
For a simple Try-Catch
block, the idea is to map the Try
statement onto the if
statement and the Catch
statement will then be the else
for the conditional. This is where we can make intelligent use of the fact that setjmp
can return different values.
If the function returns 0
, then we know that the only piece of code that ran was the code in our TRY
block. If the function returns anything else, we need to go into our CATCH
block with the same state as when we started.
We can call the longjmp
function when we THROW
an exception.
As you will see in the code below, we also need to close the TRY
block. We create an ENDTRY
function that provides the closing part of the do-while
block.
This also helps us create multiple TRY
statements within the same block. It should be noted that they cannot be nested, as we will be reusing the buf_state
variable.
An example of this implementation is below.
#include <setjmp.h>
#include <stdio.h>
#define TRY \
do { \
jmp_buf buf_state; \
if (!setjmp(buf_state)) {
#define CATCH \
} \
else {
#define ENDTRY \
} \
} \
while (0)
#define THROW longjmp(buf_state, 1)
int main() {
TRY {
printf("Testing Try statement \n");
THROW;
printf(
"Statement should not appear, as the THROW block has already thrown "
"the exception \n");
}
CATCH { printf("Got Exception \n"); }
ENDTRY;
return 0;
}
Output:
Testing Try statement
Got Exception
For practical systems, this is not enough. We need to have different types of exceptions.
The above example only supports one type of exception. Once again, we can use the different return values of setjmp
.
Instead of using if-else
, we will switch this around with a switch-case
.
The design is as follows: the TRY
statement will use a switch
statement, and CATCH
will be a macro with a parameter representing the exception type. A condition of each CATCH
statement will be that it must close the previous case
using a break
.
#include <setjmp.h>
#include <stdio.h>
#define TRY \
do { \
jmp_buf buf_state; \
switch (setjmp(buf_state)) { \
case 0:
#define CATCH(x) \
break; \
case x:
#define ENDTRY \
} \
} \
while (0)
#define THROW(x) longjmp(buf_state, x)
#define EXCEPTION1 (1)
#define EXCEPTION2 (2)
#define EXCEPTION3 (3)
int main() {
TRY {
printf("Inside Try statement \n");
THROW(EXCEPTION2);
printf("This does not appear as exception has already been called \n");
}
CATCH(EXCEPTION1) { printf("Exception 1 called \n"); }
CATCH(EXCEPTION2) { printf("Exception 2 called \n"); }
CATCH(EXCEPTION3) { printf("Exception 3 called \n"); }
ENDTRY;
return 0;
}
Output:
Inside Try statement
Exception 2 called
Add Finally
to Try-Catch
in C
We need to add a FINALLY
block for a complete functional Try-Catch
implementation. The finally
block generally executes after the try
and catch
blocks are done.
It executes regardless of whether or not an exception is thrown.
How would we implement this? The key idea is to use the default
case of the switch
case to implement the FINALLY
block.
However, the switch-case
would not run the default
case if an exception has been called in a normal case.
We will use a mechanism similar to Duff’s Device. We will essentially be intertwining a switch-case
statement with a do-while
statement.
The logic for it is something like this.
switch (an expression) {
case 0:
while (1) {
// code for case 0
break;
case 1:
// code for case 1
break;
}
default:
// code for default case
}
We use one of C’s most controversial features: switches not breaking automatically before each case label. Here, the while
statement is nested inside the switch-case
when a break
is called; it exits the while
loop and continues traversing through the cases.
In the context of our code (as shown below), the switch
cases are all our exceptions, and the default
case is in our FINALLY
block. Naturally, it will fall to the default
case, as the exception code would already have been executed.
This is shown in the code below.
#include <setjmp.h>
#include <stdio.h>
#define TRY \
do { \
jmp_buf buf_state; \
switch (setjmp(buf_state)) { \
case 0: \
while (1) {
#define CATCH(x) \
break; \
case x:
#define ENDTRY \
} \
} \
while (0)
#define THROW(x) longjmp(buf_state, x)
#define FINALLY \
break; \
} \
default:
#define EXCEPTION1 (1)
#define EXCEPTION2 (2)
#define EXCEPTION3 (3)
int main() {
TRY {
printf("Inside Try statement \n");
THROW(EXCEPTION2);
printf("This does not appear as exception has already been called \n");
}
CATCH(EXCEPTION1) { printf("Exception 1 called \n"); }
CATCH(EXCEPTION2) { printf("Exception 2 called \n"); }
CATCH(EXCEPTION3) { printf("Exception 3 called \n"); }
FINALLY { printf("This will always be called! \n"); }
ENDTRY;
return 0;
}
Output:
Inside Try statement
Exception 2 called
This will always be called!
This concludes the guide for making a try-catch
system in C. Of course, there are possible memory issues here and a few limitations (such as a lack of support for nested try-catch
systems), but this is a functional try-catch
implementation in C.
Husnain is a professional Software Engineer and a researcher who loves to learn, build, write, and teach. Having worked various jobs in the IT industry, he especially enjoys finding ways to express complex ideas in simple ways through his content. In his free time, Husnain unwinds by thinking about tech fiction to solve problems around him.
LinkedIn