Error handling

In this course, you should get accustomed to writing code defensively. More specifically, you should always include error handling code such that your code does not panic or segfault in the event of errors.

In this assignment, you will implement several functions, all of which have docstrings indicating what you should return if an error occurs during execution of the function. Make sure that whenever you call these functions in the rest of your code, you check the returned value and handle error cases appropriately.

In addition to handling errors, it is always good practice to perform argument validation at the beginning of a function. A simple example of argument validation is ensuring that pointers are not NULL.

Below are a few examples of naive code and their counterparts that handle errors appropriately.

Example 1

Without error handling:

char *new_string(char *str)
{
    return strcpy((char *) malloc(strlen(str) + 1), str);
}

malloc can potentially return a null pointer. If that happens, we will be passing a null pointer as the destination argument to strcpy, which expects a non-null buffer.

With error handling:

char *new_string(char *str)
{
    char *new_str = (char *) malloc(strlen(str) + 1);
    if (new_str == NULL) 
    {
        return NULL;
    }
    return strcpy(new_str, str);
}

Here, we decide explicitly how we want to handle the case where malloc returns a null pointer. Since the function returns a char *, it’s reasonable to return NULL (note that in this assignment, we will be clear about what you should return in error cases). Note that the caller of this function should do error handling of its own by checking if the return value of new_string is NULL.

Example 2

Without error handling:

int main(int argc, char *argv[])
{
    FILE *input_file;
    // ...
    char *file_name = ...
    input_file = fopen(file_name, "r");
    // Perform some logic using input_file.
}

Here, we perform some downstream tasks under the implicit assumption that the call to fopen succeeded and that input_file is a valid FILE *. But what if fopen fails? For example, the file with name file_name may not exist. In that case, input_file will be NULL. If we try to dereference input_file later, we can segfault or even encounter other undefined behavior that can lead to an uncontrolled crash of our program.

With error handling:

int main(int argc, char *argv[])
{
    FILE *input_file;
    // ...
    char *file_name = ...
    input_file = fopen(file_name, "r");
    if (input_file == NULL)
    {
        fprintf(stderr, "File '%s' does not exist.\n", file_name);
        return 1;
    }
    // Perform some logic using input_file.
}

Here, we address the case where fopen fails and print a helpful error message before returning the error code 1, which will get propagated to main’s caller and allow for controlled exit of the program.