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:
- They return a special value (like
NULLor-1). - They set a global variable called
errnoto 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:- Prints the string you pass to it.
- Prints a colon and a space (
:). - Prints the string message corresponding to the current value of
errno.
Crucially,
perroralways 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,
perroris the best choice.

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(usually0): Indicates the program ran successfully.EXIT_FAILURE(usually1): 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.
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
NULLor-1), the global variableerrnowill contain a code detailing the specific error. - Use
perror("Descriptive message")to print a helpful error message tostderr. 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.
-
Robust File Copy Tool: Write a program that copies a source file to a destination file.
- It should accept two command-line arguments:
source_fileanddestination_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 usingperror()and exit. - Hint: Exercise 7 from the "C Programming File Handling Exercises" page (LINK) is an excellent reference. Its solution already includes
perror.
- It should accept two command-line arguments:
-
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,mkdirtakes two arguments:mkdir(path, mode). You can use0777for the mode. mkdir()returns0on success and-1on error.- Check the return value. If it's
-1, useperror()to report why the directory creation failed (e.g., if the directory already exists or you lack permissions).
-
Multi-file
cat: Create a simplified version of thecatcommand 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
stderrusingperror()but do not exit. The program should continue and try to process the next file. This teaches non-fatal error handling.
-
User Record Validator: Write a program that tries to open a user record file named
user.data.- If the file doesn't exist (
errnoisENOENT), 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 (
errnoisEACCES), it should print a specific error message tostderr: "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.
- If the file doesn't exist (
-
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
strtolto convert each argument. - After each
strtolcall, check iferrnowas set toERANGE. If so, print a message tostderrlike "Error: '[argument]' is out of range." - Also check
endptrto 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
perroris for system/library calls that seterrno, while for logic errors (like an invalid number format), you often need customfprintf(stderr, ...)messages.
- Use