Error Handling
C does not provide direct support for error handling (also known as exception handling). By convention, the programmer is expected to prevent errors from occurring in the first place, and test return values from functions. For example, -1 and NULL are used in several functions such as socket() (Unix socket programming) or malloc() respectively to indicate problems that the programmer should be aware about. In a worst case scenario where there is an unavoidable error and no way to recover from it, a C programmer usually tries to log the error and "gracefully" terminate the program.
There is an external variable called "errno", accessible by the programs after including <errno.h> - that file comes from the definition of the possible errors that can occur in some Operating Systems (e.g. Linux - in this case, the definition is in include/asm-generic/errno.h) when programs ask for resources. Such variable indexes error descriptions accessible by the function 'strerror( errno )'.
The following code tests the return value from the library function malloc to see if dynamic memory allocation completed properly:
#include <stdio.h> /* perror */
#include <errno.h> /* errno */
#include <stdlib.h> /* malloc, free, exit */
int main(void)
{
/* Pointer to char, requesting dynamic allocation of 2,000,000,000
* storage elements (declared as an integer constant of type
* unsigned long int). (If your system has less than 2 GB of memory
* available, then this call to malloc will fail.)
*/
char *ptr = malloc(2000000000UL);
if (ptr == NULL) {
perror("malloc failed");
/* here you might want to exit the program or compensate
for that you don't have 2GB available
*/
} else {
/* The rest of the code hereafter can assume that 2,000,000,000
* chars were successfully allocated...
*/
free(ptr);
}
exit(EXIT_SUCCESS); /* exiting program */
}
The code snippet above shows the use of the return value of the library function malloc to check for errors. Many library functions have return values that flag errors, and thus should be checked by the astute programmer. In the snippet above, a NULL pointer returned from malloc signals an error in allocation, so the program exits. In more complicated implementations, the program might try to handle the error and try to recover from the failed memory allocation.
Preventing divide by zero errors
A common pitfall made by C programmers is not checking if a divisor is zero before a division command. The following code will produce a runtime error and in most cases, exit.
int dividend = 50;
int divisor = 0;
int quotient;
quotient = (dividend/divisor); /* This will produce a runtime error! */
In ordinary arithmetic division by zero is undefined. Because of this, you must check or make sure that a divisor is never zero. Alternatively, for *nix processes, you can stop the OS from terminating your process by blocking the SIGFPE signal.
The code below fixes this by checking if the divisor is zero before dividing.
#include <stdio.h> /* for fprintf and stderr */
#include <stdlib.h> /* for exit */
int main( void )
{
int dividend = 50;
int divisor = 0;
int quotient;
if (divisor == 0) {
/* Example handling of this error. Writing a message to stderr, and
* exiting with failure.
*/
fprintf(stderr, "Division by zero! Aborting...\n");
exit(EXIT_FAILURE); /* indicate failure.*/
}
quotient = dividend / divisor;
exit(EXIT_SUCCESS); /* indicate success.*/
}
Signals
In some cases, the environment may respond to a programming error in C by raising a signal. Signals are events raised by the host environment or operating system to indicate that a specific error or critical event has occurred (e.g. a division by zero, interrupt, and so on.) However, these signals are not meant to be used as a means of error catching; they usually indicate a critical event that will interfere with normal program flow.
To handle signals, a program needs to use the signal.h header file. A signal handler will need to be defined, and the signal() function is then called to allow the given signal to be handled. Some signals that are raised to an exception within your code (e.g. a division by zero) are unlikely to allow your program to recover. These signal handlers will be required to instead ensure that some resources are properly cleaned up before the program terminates.
The C Standard Library only defines six signals; Unix systems define 15 more. Each signal has a number, called a signum, associated with it. Here are a few common ones:
#define SIGHUP 1 /* Hangup the process */
#define SIGINT 2 /* Interrupt the process. C standard */
#define SIGQUIT 3 /* Quit the process */
#define SIGILL 4 /* Illegal instruction. C standard.*/
#define SIGTRAP 5 /* Trace trap, for debugging. C standard.*/
#define SIGABRT 6 /* Abort. C standard. */
#define SIGFPE 8 /* Floating Point Error. C standard. */
#define SIGSEGV 11 /* Memory error. C standard. */
#define SIGTERM 15 /* Termination request. C standard. */
Signals are handled with the signal() function, from the signal.h library. Its syntax is:
void signal(signal_to_catch, signal_handler)
Signals can be raised with raise() or kill(). raise() sends the signal to the current process; kill() sends it to a specific process.
Note that signal is now deprecated in favor of sigaction(), due to a lack of portability between Unix systems and potential for unexpected behavior. However, as sigaction()'s use is more complicated, we will stick with signal() to illustrate the concept here.
To understand how signals work, here's a simple example:
#include <stdio.h>
#include <unistd.h> // Unix Standard library, used to import sleep()
#include <stdlib.h>
#include <signal.h>
void handler(int signum) {
printf("Signal received %d, coming out...\n", signum);
exit(1);
}
int main () {
signal(SIGINT, handler); // attaching the handler() function to SIGINT signals; i.e, ctrl+c, keyboard interrupt.
while(1) {
printf("Sleeping...\n");
sleep(1000); // sleep pauses the process for a given number of seconds, or until a signal is received.
}
return(0);
}
Try compiling and testing this on your machine; after you see "Sleeping...", send the interrupt signal by pressing ctrl + c.
Here's a more complex example. This creates a signal handler and raises the signal:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
static void catch_function(int signal) {
puts("Interactive attention signal caught.");
}
int main(void) {
if (signal(SIGINT, catch_function) == SIG_ERR) {
fputs("An error occurred while setting a signal handler.\n", stderr);
return EXIT_FAILURE;
}
puts("Raising the interactive attention signal.");
if (raise(SIGINT) != 0) {
fputs("Error raising the signal.\n", stderr);
return EXIT_FAILURE;
}
puts("Exiting.");
return 0;
}
setjmp
The setjmp function can be used to emulate the exception handling feature of other programming languages. The first call to setjmp stores a reference point to the current execution point, and is valid as long as the function containing setjmp() doesn't return or exit. A call to longjmp causes the execution to return to the point of the associated setjmp call.
setjmp takes a `jmp_buf` (a type that will store an execution context) as an argument, and returns 0 the first time it runs (i.e., when it sets the return point). When it runs a second time - when longjmp is called - it then returns the value passed to longjmp.
longjmp takes a `jmp_buf` as an argument (one that's already been passed to setjmp), and a value to pass to setjmp to return.
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
int main(void) {
int val;
jmp_buf environment;
val = setjmp(environment); // val is set to 0 the first time this is called
if (val !=0)
{
printf("You returned from a longjmp call, return value is %d", val); // now, value is 1, passed from longjmp()
exit(0);
}
puts("Calling longjmp now");
longjmp(environment, 1);
return(0);
}
Try running this with a compiler on your own machine.
The values of non-volatile variables may be corrupted when setjmp returns from a longjmp call.
While setjmp() and longjmp() may be used for error handling, it is generally preferred to use the return value of a function to indicate an error, if possible. setjmp() and longjmp() are most useful when errors occur in deeply nested function calls, and it would be tedious to check return values all the way back to the point you wish to return to.
General References
Wikibooks contributors. "C Programming/Error handling". Wikibooks. Wikibooks. Retrieved 13 May 2024.