Blog Datasheets Home About me Clients My work Services Contact

G2Labs Grzegorz Grzęda

Effective Error Handling in C Programming

August 15, 2024

Effective Error Handling in C Programming

Error handling is an essential aspect of writing robust and reliable software applications. Proper error handling allows us to gracefully handle unexpected issues and prevents our programs from crashing. In this article, we will explore various techniques for effective error handling in C programming, along with extensive examples and explanations.

1. Error Codes

One common approach for error handling in C is to use error codes. A function can return an error code to indicate that something went wrong, allowing the calling code to handle the error appropriately. Here’s a simple example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>

#define SUCCESS 0
#define ERROR_DIVISION_BY_ZERO 1

int divide(int numerator, int denominator, int* result) {
    if (denominator == 0) {
        return ERROR_DIVISION_BY_ZERO;
    }
    
    *result = numerator / denominator;
    
    return SUCCESS;
}

int main() {
    int numerator = 10;
    int denominator = 0;
    int result;
    
    int error = divide(numerator, denominator, &result);
    
    if (error == SUCCESS) {
        printf("Result: %d\n", result);
    } else if (error == ERROR_DIVISION_BY_ZERO) {
        printf("Error: Division by zero!\n");
        // handle the error appropriately
    }
    
    return 0;
}

In this example, the divide function returns an error code ERROR_DIVISION_BY_ZERO if the denominator is zero, indicating a division by zero error. The calling code checks the error code returned by the function and handles the error accordingly. Using error codes can be effective for simple error handling scenarios, but it can lead to code clutter and make it harder to understand the main logic of the program.

2. Return NULL Pointers

Another approach frequently used in C is returning NULL pointers to indicate errors. This technique is commonly used when dealing with pointer-related operations such as memory allocation or resource acquisition. Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>

FILE* openFile(const char* filename, const char* mode) {
    FILE* file = fopen(filename, mode);
    
    if (file == NULL) {
        printf("Failed to open file: %s\n", filename);
    }
    
    return file;
}

int main() {
    FILE* file = openFile("example.txt", "r");
    
    if (file != NULL) {
        // perform operations on the file
        fclose(file);
    }
    
    return 0;
}

In this example, the openFile function returns a NULL pointer if it fails to open the file. The calling code checks if the returned pointer is NULL and handles the error appropriately. It’s important to note that using this technique requires proper error handling, releasing allocated resources, and preventing memory leaks.

3. Set Global Error Variable

Instead of passing error codes explicitly or returning NULL pointers, we can use a global error variable. This approach centralizes the error status, making it easier to handle errors consistently throughout the program. Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

#define SUCCESS 0
#define ERROR_DIVISION_BY_ZERO 1

int g_error = SUCCESS;

void divide(int numerator, int denominator, int* result) {
    if (denominator == 0) {
        g_error = ERROR_DIVISION_BY_ZERO;
        return;
    }
    
    *result = numerator / denominator;
}

int main() {
    int numerator = 10;
    int denominator = 0;
    int result;
    
    divide(numerator, denominator, &result);
    
    if (g_error == SUCCESS) {
        printf("Result: %d\n", result);
    } else if (g_error == ERROR_DIVISION_BY_ZERO) {
        printf("Error: Division by zero!\n");
        // handle the error appropriately
    }
    
    return 0;
}

In this example, we set the global variable g_error to indicate error conditions. The calling code checks the value of this variable and handles the error accordingly. While this technique is simpler and requires less code compared to passing error codes or returning NULL pointers, it should be used judiciously, as global variables can introduce complexity and potential issues if not managed properly.

4. Signal Handling

Signal handling is a mechanism provided by the operating system to notify a process about specific events or interruptions. In C, we can leverage signals to handle exceptional conditions and errors. Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <signal.h>

void handleSIGINT(int sig) {
    printf("Caught SIGINT signal!\n");
    // handle the signal appropriately
}

int main() {
    signal(SIGINT, handleSIGINT);
    
    printf("Waiting for SIGINT...\n");
    
    while (1) {
        // main program loop
    }
    
    return 0;
}

In this example, we register a signal handler function handleSIGINT to handle the SIGINT signal, which is typically sent by pressing Ctrl + C. When the program receives the SIGINT signal, the handler function is called. We can then perform appropriate error handling inside the handler function. Signal handling can be powerful but should be used with caution, as it may interfere with the normal execution flow of the program.

Conclusion

Effective error handling is crucial for writing robust and reliable C programs. By using techniques such as error codes, returning NULL pointers, setting global error variables, or handling signals, we can gracefully handle exceptional conditions and prevent our programs from crashing. Understanding these error handling techniques and applying them appropriately will greatly enhance the stability and usability of our C programs.


➡️ Tips and Best Practices for nRF52 Development


⬅️ Debugging Techniques for nRF52 Projects


Go back to Posts.