Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 6

Error Handling With `perror` and `errno`

Recommended resources

Hello! Welcome to your sixth lesson on C programming.

In our last lesson, we built our first interactive command-line tools using argc and argv. We saw that robust programs need to handle unexpected input, and you got a brief preview of errno when we used strtol to convert strings to numbers. Failures are a fact of life in programming, and in a low-level language like C, handling them correctly is not just good practice—it's essential for writing stable and secure software.

Today, we will formalize our approach to dealing with runtime problems. The learning outcome for this lesson is to implement basic error handling by checking function return values and using perror and errno. This is a cornerstone of professional C programming and is fundamentally different from the try/catch exception handling you might be used to in languages like Python, Java, or even modern PHP.

Why Error Handling in C is Different

In many high-level languages, when a function like "read file" fails, it "throws an exception." The runtime environment halts the normal program flow and searches for an exception handler (catch block).

C doesn't have this mechanism. Instead, functions that can fail (especially those interacting with the operating system, like file I/O or memory allocation) typically signal an error in two ways:

  1. They return a special value (like NULL or -1).
  2. They set a global variable called errno to a specific code that describes why it failed.

Your job as the programmer is to manually check the return value, and if it indicates an error, inspect errno to understand and report the problem. Ignoring this is a recipe for silent failures, crashes, and security vulnerabilities.


The Standard Error Handling Pattern

Let's explore this pattern using a common task: opening a file.

A program that simply tries to open a file without any error checking is very fragile. It might fail silently, leaving the user confused. The video below demonstrates this problem and then walks through the standard C solution step-by-step.

Handling Errors in C/Unix (perror, strerror, errno)

The video 'Handling Errors in C/Unix' by Jacob Sorber provides an excellent overview of the problem and the solution. It clearly explains the roles of return values, errno, strerror, and perror.

Watch from the beginning to 04:58. This covers the core of today's lesson. Pay close attention to these key points: How checking man pages reveals the error-signaling behavior of functions (e.g., open returning -1). What errno is and why it's only meaningful right after a failed call. How strerror() translates the numeric errno code into a human-readable message. How perror() provides a convenient, one-line way to report the error to the standard error stream (stderr).

Let's break down the key components discussed in the video.

1. Check the Return Value

Most library functions that can fail have their error return values documented. For fopen() (from <stdio.h>), the documentation says it returns a FILE pointer on success and NULL on failure.

#include <stdio.h>

FILE *fp = fopen("non_existent_file.txt", "r");

if (fp == NULL) {
    // An error occurred! But why?
}

This check is the first and most crucial step. If you don't check for NULL, your program will likely crash on the next line when it tries to use the invalid pointer fp (a segmentation fault).

2. The errno Global Variable

When fopen() returns NULL, it also sets a global integer variable named errno. You must include <errno.h> to use it. The value of errno is a code that corresponds to the specific error. For example:

  • 2 (ENOENT): No such file or directory.
  • 13 (EACCES): Permission denied.

3. Making errno Understandable: strerror and perror

Printing a raw number like errno: 2 isn't very user-friendly. We have two functions to help.

  • strerror(errno): This function, from <string.h>, takes an error number and returns a pointer to a descriptive string.

    // #include <string.h>
    // #include <errno.h>
    printf("Error: %s\n", strerror(errno));
    // Possible output: "Error: No such file or directory"
    
  • perror(const char *s): This function, from <stdio.h>, is even more convenient. It does three things in one call:

    1. Prints the string you pass to it.
    2. Prints a colon and a space (: ).
    3. Prints the string message corresponding to the current value of errno.

    Crucially, perror always writes to the standard error stream (stderr), which is the correct place for error messages.

    // #include <stdio.h>
    if (fp == NULL) {
        perror("Failed to open file");
        // Possible output: "Failed to open file: No such file or directory"
    }
    

    For most simple command-line tools, perror is the best choice.

C Error Handling with errno, perror, and strerror
This diagram summarizes the error handling flow in C. A function call failure sets `errno`. We can then use `perror` or `strerror` to get a human-readable message, which should be printed to `stderr`.
Test your understanding!

Imagine fopen("secret.txt", "r") fails because you don't have read permissions for the file secret.txt. What would you expect perror("Access denied") to print to the console?

Show answer

It would print the string you provided, followed by the system error message for a permissions error:
Access denied: Permission denied


Exiting a Program on Failure

When a critical error occurs and the program cannot continue, you should terminate it and signal to the operating system that something went wrong. This is done by exiting with a non-zero status code.

The <stdlib.h> header provides two macros for this purpose:

  • EXIT_SUCCESS (usually 0): Indicates the program ran successfully.
  • EXIT_FAILURE (usually 1): Indicates the program terminated due to an error.

The exit() function immediately terminates the program, and the value you pass is returned to the shell. This is essential for scripting, as a shell script can check the exit code of your program to see if it succeeded.

Let's combine everything into a robust code snippet.

Error Handling in C

The article 'Error Handling in C' from GeeksforGeeks provides a clear example of using exit() along with error-reporting functions.

Read the section titled 'Exit Status'. Focus on the code example. See how it checks the return value of fopen, uses perror and strerror to report the issue, and then calls exit(EXIT_FAILURE) to terminate.

The canonical pattern for a fatal error looks like this:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main(int argc, char *argv[]) {
    // Basic argument check (from last lesson)
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    FILE *fp = fopen(argv[1], "r");
    
    // The core error handling pattern
    if (fp == NULL) {
        perror("Error opening file");
        exit(EXIT_FAILURE);
    }
    
    printf("File '%s' opened successfully.\n", argv[1]);
    
    // ... do work with the file ...
    
    fclose(fp);
    
    return EXIT_SUCCESS; // or simply return 0
}

Conclusion

You now have the fundamental toolkit for writing robust C programs that can gracefully handle runtime errors. Unlike higher-level languages that hide this complexity, C puts you in full control, which is essential for building the reliable systems software you're interested in.

Key Takeaways:

  • Always check the return values of functions that can fail (e.g., fopen, malloc).
  • If a function returns an error indicator (like NULL or -1), the global variable errno will contain a code detailing the specific error.
  • Use perror("Descriptive message") to print a helpful error message to stderr. This is the easiest and often best method.
  • Use strerror(errno) if you need more control over formatting the error message.
  • When a fatal error occurs, terminate the program with exit(EXIT_FAILURE) to signal failure to the operating system.

In our next lesson, we will look at another essential technique for understanding your program's behavior: using printf for basic debugging and tracing. While perror and errno tell you about system-level failures, printf debugging is your go-to tool for finding logical errors in your own code.



Practice Exercises

Now, let's apply these concepts. These exercises will help you build robust command-line tools that handle failure gracefully.

  1. Robust File Copy Tool: Write a program that copies a source file to a destination file.

    • It should accept two command-line arguments: source_file and destination_file.
    • Implement checks for the correct number of arguments.
    • Use fopen() to open the source file in read mode ("r") and the destination file in write mode ("w").
    • Use the error handling pattern for both fopen() calls. If either fails, report the error using perror() and exit.
    • Hint: Exercise 7 from the "C Programming File Handling Exercises" page (LINK) is an excellent reference. Its solution already includes perror.
  2. Simple mkdir: Write a program that creates a new directory.

    • It should take one command-line argument: the name of the directory to create.
    • Use the mkdir() function (from <sys/stat.h> and <sys/types.h>). On Linux/macOS, mkdir takes two arguments: mkdir(path, mode). You can use 0777 for the mode.
    • mkdir() returns 0 on success and -1 on error.
    • Check the return value. If it's -1, use perror() to report why the directory creation failed (e.g., if the directory already exists or you lack permissions).
  3. Multi-file cat: Create a simplified version of the cat command that can print the contents of multiple files.

    • The program should take one or more filenames as command-line arguments.
    • Loop through the arguments (argv[1], argv[2], etc.).
    • For each filename, attempt to open and read it.
    • Crucially: If a file cannot be opened, print an error to stderr using perror() but do not exit. The program should continue and try to process the next file. This teaches non-fatal error handling.
  4. User Record Validator: Write a program that tries to open a user record file named user.data.

    • If the file doesn't exist (errno is ENOENT), the program should create it in write mode ("w") and print a message: "No user record found. Created a new one."
    • If the file exists but the program lacks permission to read it (errno is EACCES), it should print a specific error message to stderr: "Error: Cannot access user record. Check permissions." and exit.
    • For any other file opening error, it should use perror() to print a generic message and exit.
  5. Error-Checked Summing Tool: Revisit the summing tool from the last lesson, but with stricter error handling. The program takes any number of integer arguments and prints their sum.

    • Use strtol to convert each argument.
    • After each strtol call, check if errno was set to ERANGE. If so, print a message to stderr like "Error: '[argument]' is out of range."
    • Also check endptr to ensure the entire string was consumed. If not, print "Error: '[argument]' is not a valid integer."
    • The program should skip invalid or out-of-range numbers but continue to sum the valid ones.
    • This exercise highlights that perror is for system/library calls that set errno, while for logic errors (like an invalid number format), you often need custom fprintf(stderr, ...) messages.