Introduction to Error Handling in C

Key Takeaways
  • An error is an anomalous condition that can occur during program execution, preventing its normal functioning.
  • There are different types of errors: compilation errors, logical errors, and runtime errors (exceptions).
  • In C, there is no built-in exception handling system, but it is possible to implement strategies to handle runtime errors.
  • A common strategy in C is to use special return values or output parameters to signal errors to calling functions.
  • Many C standard library functions use this approach to signal errors, and programmers must always check these values to properly handle error conditions.

What is an Error

In general, during program execution, anomalous conditions may occur that do not allow its normal functioning.

These anomalous conditions are called Errors.

However, we must make proper distinctions among the various types of errors that can occur in a program.

  1. Compilation Errors: are errors that occur during the compilation phase of source code. These errors prevent the compiler from translating the code into machine language. Common examples include syntax errors, invalid variable declarations, or the use of undefined functions.
  2. Logical Errors: are errors that occur during program execution, but are due to incorrect logic implemented by the programmer. These errors could cause the program to crash or produce unexpected results. For example, an infinite loop or an incorrect mathematical calculation. These errors are often called Bugs.
  3. Runtime Errors: are errors that occur during program execution, but are not necessarily due to logical errors. These errors can be caused by conditions external to the program, such as lack of memory, accessing non-existent files, or incorrect input from the user. This type of error is often called an Exception.

In the first two cases, the responsibility for the error falls on the software developers who can therefore correct them by modifying the source code.

The third case, however, is more complex to handle as the conditions that cause it can be unpredictable and beyond the developer's control.

For example, a program that attempts to open a file might not succeed if the file does not exist or if the user does not have the necessary permissions. Or, a program that requires a number as input might receive a non-numeric value, causing an error during conversion.

The point is that while the first two types of errors are handled during development, exceptions must be handled during program execution to ensure that the software can continue to function correctly even in the presence of anomalous conditions.

More modern languages, such as C++, Java, and Python, offer built-in mechanisms for exception handling, allowing developers to write more robust and resilient code.

In C, unfortunately, there is no exception handling system built into the language. Nevertheless, it is possible to implement strategies to handle runtime errors effectively.

Error Handling Strategies in C

To understand how to handle errors in C, it is important to ask fundamental questions.

Since, ultimately, all C code can be viewed as a series of functions calling each other, when we write a function, it might encounter an error during its execution. In general, that function might be able to handle the error itself, or it might be necessary for that function to signal or communicate the error to the calling function, so that the latter can handle it.

Let's start with a simple example where we have a function, read_number, that reads an integer from standard input. We have two possibilities:

  1. If the input is not valid (for example, the user enters a string instead of a number), the read_number function can handle the error internally, for example by asking the user to enter the number again.
  2. If the input is not valid, the read_number function can signal the error to the calling function.

The first case can be handled with a while loop that continues to ask for input until a valid value is provided:

#include <stdio.h>

int read_number() {
    int number;
    int result;
    while (1) {
        printf("Enter an integer: ");
        result = scanf("%d", &number);
        if (result == 1) {
            return number; // Valid input
        } else {
            printf("Invalid input. Try again.\n");
            // Clear the input buffer
            while (getchar() != '\n');
        }
    }
}

The read_number function handles the error internally, asking the user to try again in case of invalid input.

In the second case, instead, the read_number function must communicate the error to the calling function. Typically, the classic approach in C is to use a special return value to indicate an error, or use an output parameter to return the operation status.

The read_number function, however, returns an integer, so we cannot use the return value to signal an error without losing the number read. So, we must modify the function to use an output parameter, that is a pointer to integer, to return the number read, and use the return value to signal the operation status:

#include <stdio.h>

int read_number(int *number) {
    int result;
    printf("Enter an integer: ");
    result = scanf("%d", number);
    if (result == 1) {
        return 0; // Success
    } else {
        // Clear the input buffer
        while (getchar() != '\n');
        return -1; // Error
    }
}

int main() {
    int number;
    int status = read_number(&number);
    if (status == 0) {
        printf("You entered the number: %d\n", number);
    } else {
        printf("Error: invalid input.\n");
    }
    return 0;
}

In this example, the read_number function returns 0 in case of success and -1 in case of error. The calling function (main) can then check the operation status and handle the error accordingly.

This error handling strategy is very common in C language, to the point that many C standard library functions use this approach to signal errors. Think for example of I/O functions like fopen, fread, fwrite, etc. These functions return special values (such as NULL or EOF) to indicate errors, and the programmer must always check these values to properly handle error conditions.

Therefore, we can construct a general scheme for error handling in C:

int value;

value = function_that_can_fail(parameters);

if (value indicates success) {
    // Proceed with normal processing
} else {
    // Handle the error
}

Using a return value to signal errors also has another advantage. Since we are using an integer, we do not have to limit ourselves to two states (success/error). We can define a series of error codes to represent different error conditions, thus allowing finer exception handling.

For example, we could define error codes as follows:

#define SUCCESS 0
#define ERROR_FILE_NOT_FOUND -1
#define ERROR_READ -2
#define ERROR_WRITE -3

int function_that_can_fail(parameters) {
    // Function logic
    if (file_not_found) {
        return ERROR_FILE_NOT_FOUND;
    }
    if (read_error) {
        return ERROR_READ;
    }
    if (write_error) {
        return ERROR_WRITE;
    }
    return SUCCESS;
}

In this way, we are not only signaling that an error has occurred, but we are also providing specific information about the type of error that occurred, allowing the calling function to handle it more appropriately.

In conclusion, even though the C language does not offer a built-in exception handling system, it is possible to implement effective strategies to handle runtime errors, thus ensuring the robustness and reliability of the developed software.

In the next lesson we will explore a second exception handling technique in C, used by some standard library functions, which is based on the use of the global variable errno.