Overview
Error handling is an essential part in programming. Due to the nature of programming, there are errors that occurs in runtime that programs just cannot possibly handle. Common source of error of this kind includes user input and hardware states, which programs cannot control or predict.
There are generally three ways to handle errors,
- Program exits at error
- Returns an indication of error
- Sets a global variable in the case of error
Additionally, exception, which is not supported by C but is a common and powerful way to handle errors.
Program exits at error
// exit_at_error.c // gcc -o exit_at_error -std=c11 exit_at_error.c #include <stdio.h> #include <assert.h> #include <stdlib.h> float divide(float dividend, float divisor) { if (divisor == 0) { printf("divisor cannot be zero\n"); exit(2); } return dividend / divisor; } int main() { float dividend, divisor, result; int n_char_read = 0; while (1) { printf("enter dividend: "); n_char_read = scanf("%f", ÷nd); if (n_char_read != 1) break; printf("enter divisor: "); n_char_read = scanf("%f", &divisor); if (n_char_read != 1) break; result = divide(dividend, divisor); printf("%f / %f = %f\n", dividend, divisor, result); } return 0; }
The
float divide(float a, float b) function calculates the quotient given the dividend and divisor. If the divisor is zero, the function will call the exit() library function and in effect kills the program. Similar functions include abort and assert.The pros and cons of this way of handling error is obvious. It is simple and sometimes too simple and primitive. It does not give the caller a chance to correct the error and messes up the entire stack because it exits the program in the middle of the program.
It is only acceptable for simple tools or scripts. It is discouraged to process error in this way in any long lasting daemon programs or any complicated and sophisticated programs.
Returns an indication of error
Returning an indication of error is the most common way of error handling in C and extensions of C. For example, all functions from CUDA C runtime API returns a variable indication of error.
More generally, the returned value can be viewed as indication for the result of the calculation, which in essence is what returned value actually mean. For our
float divide(float a, float b) function, any value in the range of float could be returned. One way to deal with this situation is to introduce an output parameter. // return_indication.c // gcc -o return_indication -std=c11 return_indication.c #include <stdio.h> #include <assert.h> #include <stdlib.h> #include <stdbool.h> bool divide(float dividend, float divisor, float *result) { if (divisor == 0) { return false; } *result = dividend / divisor; return true; } int main() { float dividend, divisor, result; int n_char_read = 0; bool success = false; while (1) { printf("enter dividend: "); n_char_read = scanf("%f", ÷nd); if (n_char_read != 1) break; printf("enter divisor: "); n_char_read = scanf("%f", &divisor); if (n_char_read != 1) break; success = divide(dividend, divisor, &result); if (success) { printf("%f / %f = %f\n", dividend, divisor, result); } else { printf("divisor cannot be zero\n"); } } return 0; }
In the above example, we change the signature of our divide function to
bool divide(float dividend, float divisor, float *result), where result is the actual quotient that comes from the division and the returned boolean is an indication of success (or error for that matters).Sets a global variable in the case of error
Setting a global variable as indication of error can be viewed as a special case of returning an indication where the indication of error is no longer returned to the caller but rather stored in a variable with static storage that the caller checks afterwards. (Strictly speaking, since C11
errno has thread-local storage, so each thread gets its own copy.) This is the traditional way of handling error in some library in C, such as the math library and the stdlib library.// set_global_variable.c // gcc -o set_global_variable -std=c11 set_global_variable.c #include <stdio.h> #include <errno.h> #include <stdlib.h> #include <ctype.h> int main() { char str[128]; char *endPtr = str; long result = 0; int num_char_read = 0; while (1) { printf("enter a number: "); num_char_read = scanf("%127s", str); if (num_char_read != 1 || !isdigit(str[0])) { printf("$ you enter an invalid value, exiting ...\n"); break; } errno = 0; result = strtol(str, &endPtr, 10); if (errno == ERANGE) { printf("$ your input is too large for long interger type\n"); } else { printf("$ you enter %ld\n", result); } } return 0; }
In this example, the program tries to convert the user input string into a long integer type. After the call to
strtol, it checks a global variable errno that’s defined in the errno.h header file. The errno variable will be set to ERANGE if either underflow or overflow occurs. Try to enter 999999999999999999999999999999 and see what happens.It is worth noting that CUDA C, because of its asynchronous nature, also adapts this way of error handling for its asynchronous functions. It sets a global indication of error that can be check by calling
cudaGetLastError. For more information, see the error checking section of the CUDA programming guide.Exceptions
Exception handling is a powerful mechanism for dealing with unexpected and potentially catastrophic errors in programming. It empowers functions to gracefully transfer control back to the caller, enabling the proper handling of unforeseen issues. Many modern programming languages, including Python, C++, and Java, embrace the exception control flow, often employing a
try...catch... paradigm. However, the C programming language, renowned as one of the oldest and most enduring languages in the world, notably lacks native support for exceptions. Here I include an exception handling demo in C++ just for your reference.
// exception.cpp // g++ -o exception -std=c++14 exception.cpp #include <iostream> using namespace std; int main() { try { throw(20); } catch (int e) { cout << "An exception occurred. Exception Nr. " << e << '\n'; } return 0; }
For more on how exceptions work in C++, see my post on C++ Exceptions.
That said, C does have something that comes close:
setjmp and longjmp from setjmp.h. setjmp saves the current execution context and returns 0. A later call to longjmp jumps back to that saved point, and setjmp returns a second time, now with the value passed to longjmp. Used together, they look a lot like try and throw.// setjmp_longjmp.c // gcc -o setjmp_longjmp -std=c11 setjmp_longjmp.c #include <setjmp.h> #include <stdio.h> static jmp_buf env; float divide(float dividend, float divisor) { if (divisor == 0) { longjmp(env, 1); // like "throw" } return dividend / divisor; } int main() { if (setjmp(env) == 0) // like "try" { printf("1 / 2 = %f\n", divide(1, 2)); printf("1 / 0 = %f\n", divide(1, 0)); printf("never printed\n"); } else // like "catch" { printf("divisor cannot be zero\n"); } return 0; }
It prints
1 / 2 = 0.500000 divisor cannot be zero
Some well-known C libraries handle errors this way, libpng and the Lua interpreter among them. But it is a sharp tool:
- Nothing gets cleaned up. Memory allocated or files opened between
setjmpandlongjmpare simply abandoned, and unlike C++ there are no destructors to release them.
- Local variables that are modified after
setjmphave indeterminate values after the jump, unless they are declaredvolatile.
- You can only jump back into a function that is still running. Calling
longjmpafter the function that calledsetjmphas returned is undefined behavior.
Summary
Approach | Pros | Cons | Typically seen in |
Program exits at error | Simplest possible | The caller gets no chance to recover; nothing is cleaned up | Small tools and scripts |
Returns an indication of error | Explicit and cheap; the caller decides what to do | Easy to ignore the returned value; real results have to go through output parameters | Most C APIs, POSIX, the CUDA runtime API |
Sets a global variable | The return value stays free for the actual result | Easy to forget to reset and check it; the next call may overwrite it | errno in the C standard library, cudaGetLastError |
Exceptions | Cannot be silently ignored; keeps error handling apart from the normal flow | Hidden control flow; costly when thrown; not available in C ( setjmp/longjmp comes closest) | C++, Python, Java |
There is no single best approach. In C, returning an indication of error is the default choice, and it is worth checking every returned value, especially from
scanf, malloc and friends, which are the easiest ones to forget. Exiting at error is fine for a throwaway script, but not for anything that runs for long or that other code depends on.