Error handling

Error handling

Tags
Programming
C
Published
October 2, 2021
Author
yanbc

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,
  1. Program exits at error
  1. Returns an indication of error
  1. 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", &dividend); if (n_char_read == 0) break; printf("enter divisor: "); n_char_read = scanf("%f", &divisor); if (n_char_read == 0) 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() system function and in effect kills the program. Similar system calls include exit, 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", &dividend); if (n_char_read == 0) break; printf("enter divisor: "); n_char_read = scanf("%f", &divisor); if (n_char_read == 0) 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 not longer returned from stack but rather set and shared in the heap storage space. 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("%s", str); if (num_char_read == 0 || !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; }