Chapter 91: Error Handling in C Applications
Chapter Objectives
Upon completing this chapter, you will be ableto:
- Understand the fundamental principles of error handling in C-based Linux applications and their critical importance in embedded systems.
- Implement robust error-checking mechanisms by correctly interpreting function return codes to detect failures.
- Utilize the
errno
variable in conjunction withperror()
andstrerror()
to diagnose the specific cause of system call and library function errors. - Develop C applications that gracefully handle runtime errors, preventing crashes and ensuring system stability on an embedded platform like the Raspberry Pi 5.
- Debug common error-handling pitfalls, such as incorrect
errno
usage and ignoring function return values. - Apply best practices for writing reliable and maintainable code that anticipates and manages potential failures in a resource-constrained environment.
Introduction
In the world of embedded systems, failure is not merely an inconvenience; it can be catastrophic. An unhandled error in a medical device, an automotive control unit, or an industrial automation system can lead to equipment damage, financial loss, or even endanger human life. Unlike desktop applications, where a crash might simply require a restart, embedded systems often operate autonomously in mission-critical roles where reliability is paramount. This unforgiving operational context elevates error handling from a mere “best practice” to a foundational pillar of system design. For developers working with Embedded Linux on platforms like the Raspberry Pi 5, mastering the art of anticipating, detecting, and responding to errors is a non-negotiable skill.
This chapter delves into the primary mechanisms for error handling in C programs running on a POSIX-compliant operating system like Linux. We will move beyond the simplistic “happy path” of programming, where every function call is assumed to succeed, and confront the reality of runtime complications. You will learn that the C standard library and the underlying Linux system calls have a well-defined, albeit sometimes subtle, contract for communicating failure. The two core components of this contract are function return codes and the global integer variable, errno
. By meticulously checking the values returned by functions, we get the first signal that something has gone wrong. Subsequently, by inspecting errno
, we can uncover the specific reason for the failure, much like a detective using an initial clue to uncover the detailed story of a crime. Throughout this chapter, we will use the Raspberry Pi 5 as our practical development platform to write, compile, and test code that is resilient, robust, and ready for the rigors of the real world.
Technical Background
At the heart of system programming in a C and Linux environment lies a simple yet powerful convention for communicating success or failure. Most system calls and a vast number of C library functions are designed to report their status through their return value. This mechanism serves as the primary and most immediate signal to the calling program about the outcome of its request. Ignoring this signal is one of the most common and dangerous mistakes a programmer can make.

The Contract of Return Codes
The convention, while not universally rigid, is remarkably consistent across the POSIX API. For a great many functions, a return value of 0 indicates success, while a return value of -1 signals an error. This is particularly true for functions that perform I/O operations or interact with the kernel to manage resources. Consider the close()
system call, used to terminate a connection to a file descriptor. If it successfully closes the file, it returns 0. If it fails for some reason—perhaps the file descriptor was invalid to begin with—it returns -1.
However, this is not the only pattern. Another common paradigm is used by functions that are expected to return a non-negative value on success. For instance, the open()
system call, which establishes a connection to a file, returns a file descriptor—a small, non-negative integer—on success. Here, the error condition is still signaled by a return value of -1. Similarly, the read()
and write()
functions return the number of bytes successfully read or written. Since it’s impossible to read or write a negative number of bytes, -1 is again reserved as the universal signal for an error.
Pointers also play a crucial role in this error-reporting scheme. Functions that allocate memory or create complex data structures, such as malloc()
from the standard library or mmap()
for memory-mapping files, return a pointer to the newly allocated resource on success. If the allocation fails, they return a NULL
pointer. In the context of pointers, NULL
is the equivalent of -1 for integers; it is a reserved value that unambiguously indicates failure.
The critical takeaway is that the return value is the gatekeeper. Before your program proceeds, it must inspect this value. If you call open()
and immediately attempt to use the returned file descriptor without checking if it’s -1, you are making a dangerous assumption. If open()
failed, you will be passing an invalid file descriptor (-1) to subsequent functions like read()
or write()
, which will in turn fail, cascading the error through your application and leading to undefined behavior.
errno
: The Reason for the Failure
Detecting that an error has occurred is only half the battle. A return code of -1 tells you that something went wrong, but it doesn’t tell you what went wrong. Did open()
fail because the file does not exist? Or did it fail because your program lacks the necessary permissions to read it? Or perhaps the path pointed to a directory, not a regular file? To answer these “why” questions, we turn to the second piece of the error-handling puzzle: the errno
variable.
errno
is a global integer variable defined in the <errno.h>
header. When a system call or a C library function fails, it not only returns -1 (or NULL
) but also sets errno
to a positive integer value that corresponds to the specific error. The Linux kernel maintains a standard set of these error codes, each with a symbolic name defined in <errno.h>
. For example, if open()
fails because the file doesn’t exist, errno
will be set to the value represented by the macro ENOENT
(“Error, No Entry”). If it fails due to a permissions issue, errno
will be set to EACCES
(“Error, Access Denied”).
graph TD subgraph "User Space (C Application)" A[<b>Start:</b> C Program] --> B{"Call <b>open(file.txt, O_RDONLY)</b>"}; B --> C[Execution Pauses, <br>Waits for Kernel]; G --> H{"Check Return Value<br><b>if (fd == -1)</b>"}; H -- Yes (Error) --> I[Read <b>errno</b> value <br>to determine cause]; I --> J["<b>Handle Error:</b><br>Call perror(open failed)<br>or use strerror(errno)"]; J --> K[End: Exit or Recover]; H -- No (Success) --> L[Use valid file descriptor 'fd']; L --> M[<b>End:</b> Continue Program]; end subgraph "Kernel Space (Linux OS)" C --> D{Kernel attempts to open file}; D -- Failure <br>(e.g., file not found) --> E[1. Set Process's <b>errno</b><br>e.g., errno = ENOENT]; E --> F[2. Prepare return value<br><b>-1</b>]; F --> G[Return to User Space]; D -- Success --> P[1. Allocate File Descriptor<br>e.g., fd = 3]; P --> Q[2. Prepare return value<br><b>3</b>]; Q --> G; end linkStyle 0 stroke-width:2px,fill:none,stroke:gray; linkStyle 1 stroke-width:2px,fill:none,stroke:gray; linkStyle 2 stroke-width:2px,fill:none,stroke:gray; linkStyle 3 stroke-width:2px,fill:none,stroke:gray; linkStyle 4 stroke-width:2px,fill:none,stroke:red; linkStyle 5 stroke-width:2px,fill:none,stroke:red; linkStyle 6 stroke-width:2px,fill:none,stroke:red; linkStyle 7 stroke-width:2px,fill:none,stroke:green; linkStyle 8 stroke-width:2px,fill:none,stroke:green; linkStyle 9 stroke-width:2px,fill:none,stroke:gray; linkStyle 10 stroke-width:2px,fill:none,stroke:red; linkStyle 11 stroke-width:2px,fill:none,stroke:red; linkStyle 12 stroke-width:2px,fill:none,stroke:red; linkStyle 13 stroke-width:2px,fill:none,stroke:green; linkStyle 14 stroke-width:2px,fill:none,stroke:green; style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style B fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style C fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style D fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff style E fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff style F fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff style P fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff style Q fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff style G fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style H fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style I fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style J fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff style L fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style K fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff style M fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
It is crucial to understand the precise lifecycle of errno
. Its value is only meaningful immediately after a function has returned an error indicator. A successful function call is not required to leave errno
unchanged. Therefore, the correct programming pattern is always to check the return value first. Only if the return value indicates an error should you then inspect errno
.
// Correct pattern for checking errno
int fd = open("myfile.txt", O_RDONLY);
if (fd == -1) {
// An error occurred. NOW it is safe and meaningful to check errno.
// ... handle the error based on the value of errno ...
}
Attempting to check errno
without this gatekeeping check is a recipe for bugs. A previous, unrelated function call might have failed and set errno
, and your program would be incorrectly diagnosing an error that never happened in the current operation.
Furthermore, errno
is defined by the C standard as a thread-local variable. This is a critical feature for modern multi-threaded applications. If it were a true global variable, one thread could fail a system call and set errno
, only to have that value overwritten by another thread’s system call before the first thread has a chance to read it. By making it thread-local, each thread of execution gets its own private copy of errno
, preventing these race conditions and ensuring that error diagnosis remains reliable in concurrent programs.
Translating errno
into Human-Readable Messages
While macros like ENOENT
and EACCES
are perfect for programmatic checks (e.g., if (errno == ENOENT)
), they are not helpful for logging or displaying messages to a user. A message like “Error code 2” is far less informative than “No such file or directory.” To bridge this gap, the C library provides two essential functions: strerror()
and perror()
.
The strerror()
function, defined in <string.h>
, takes an error number (like the value of errno
) as an argument and returns a pointer to a human-readable string describing that error.
// Using strerror()
#include <string.h>
#include <errno.h>
#include <stdio.h>
// ... inside a function after an error ...
if (fd == -1) {
// Get the descriptive string for the current errno value
char *error_message = strerror(errno);
fprintf(stderr, "Failed to open file: %s\n", error_message);
}
This allows you to build custom, detailed error messages that are invaluable for debugging and logging.
The perror()
function, defined in <stdio.h>
, provides an even more convenient shortcut. It takes a single string argument, which it prints to the standard error stream (stderr
), followed by a colon, a space, and then the human-readable error message corresponding to the current value of errno
.
// Using perror()
#include <stdio.h>
#include <errno.h>
// ... inside a function after an error ...
if (fd == -1) {
// perror does the work of printing our prefix and the system error message
perror("Failed to open file");
}
If errno
was ENOENT
, this call to perror()
would print the following to stderr
:
Failed to open file: No such file or directory
This is often the quickest and most effective way to report a system error, as it combines a programmer-defined context (“Failed to open file”) with the system’s specific diagnosis (“No such file or directory”). Using stderr
is also a critical practice. Standard output (stdout
) is for the program’s primary output, while stderr
is the designated channel for error messages and diagnostics. This allows a user to redirect the normal output to a file while still seeing error messages on the console, a common and powerful technique in the command-line world.
graph TD subgraph Application A(<b>errno</b> variable<br><i>Value: 2</i>); A --> B("<b>perror(Failed to open)</b>"); A --> C("<b>strerror(errno)</b>"); B --> D{Prints directly to <b>stderr</b>}; C --> E{Returns <b>char*</b>}; E --> F["<b>fprintf(stderr, Error: %s, ptr)</b>"]; F --> G{Prints formatted string to <b>stderr</b>}; end subgraph "C Library (libc)" B --> L1["Reads <b>errno</b> value (2)"]; C --> L2["Reads error num argument (2)"]; L1 & L2 --> M{Lookup in internal<br>error string table}; M -- "2" --> N["No such file or directory"]; end N --> E; N -- Message --> B; style A fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937 style B fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style C fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style D fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff style E fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style F fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style G fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff style L1 fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff style L2 fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff style M fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff style N fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff
In summary, the combination of return codes and errno
forms a robust, two-stage error-handling system. The return code is the “if,” and errno
is the “why.” A disciplined programmer on the Linux platform internalizes this pattern: check the return value on every single system call and library function that can fail. If and only if it indicates an error, use errno
with perror()
or strerror()
to report a precise, informative diagnostic message. This discipline is the bedrock of reliable system software.
Practical Examples
Theory provides the foundation, but skill is built through practice. In this section, we will apply our understanding of return codes and errno
to practical scenarios on the Raspberry Pi 5. We will write, compile, and run C code that interacts with the filesystem, deliberately provoking errors to see how the system reports them.
For these examples, you will need access to your Raspberry Pi 5’s command line, either directly or via an SSH connection. We will use the standard gcc
compiler, which is part of the Raspberry Pi OS toolchain.
Example 1: Handling a Non-Existent File
Our first task is to write a program that attempts to open a file for reading. We will first try with a file that doesn’t exist to see the ENOENT
error in action.
File Structure and Code
Create a directory for our chapter work and create a C file named open_test.c
.
mkdir ~/ch91_examples
cd ~/ch91_examples
nano open_test.c
Enter the following C code into the open_test.c
file. This program takes a single command-line argument—the name of the file to open—and attempts to open it.
// open_test.c: Demonstrates basic error handling for the open() system call.
#include <stdio.h> // For perror(), fprintf()
#include <stdlib.h> // For exit()
#include <fcntl.h> // For open() and O_RDONLY
#include <unistd.h> // For close()
#include <errno.h> // For errno
int main(int argc, char *argv[]) {
// A C program's name is always the first argument.
// We expect one additional argument: the filename.
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
exit(EXIT_FAILURE);
}
char *filepath = argv[1];
int fd; // File descriptor
printf("Attempting to open file: %s\n", filepath);
// The core operation: attempt to open the file for reading.
// O_RDONLY is a flag that specifies read-only access.
fd = open(filepath, O_RDONLY);
// THE CRITICAL CHECK: Is the return value -1?
if (fd == -1) {
// If we are here, open() failed.
// Now, and only now, we can reliably inspect errno.
// perror() is the easiest way to report the error.
// It prints our custom message, followed by the system's error description.
perror("Error opening file");
// We can also check errno directly for specific error codes.
if (errno == ENOENT) {
fprintf(stderr, "Diagnostic: The file does not appear to exist.\n");
} else if (errno == EACCES) {
fprintf(stderr, "Diagnostic: Permission denied. Cannot read the file.\n");
}
// It's critical to exit with a failure status.
exit(EXIT_FAILURE);
}
// If we reach this point, open() was successful.
printf("File opened successfully. File descriptor is: %d\n", fd);
// Good practice: always close the file descriptor when done.
if (close(fd) == -1) {
perror("Error closing file");
exit(EXIT_FAILURE);
}
printf("File closed successfully.\n");
return EXIT_SUCCESS;
}
Build and Execution Steps
1. Compile the Code: Use gcc
to compile the program. The -o
flag specifies the name of the output executable.
gcc open_test.c -o open_test
2. Run with a Non-Existent File: Execute the program, passing the name of a file you know does not exist.
./open_test no_such_file.txt
Expected Output and Analysis
You should see the following output on your console:
Attempting to open file: no_such_file.txt
Error opening file: No such file or directory
Diagnostic: The file does not appear to exist.
Let’s break this down:
Attempting to open file...
: This is our program’s own status message fromprintf
.Error opening file: No such file or directory
: This is the output fromperror()
. Our custom prefix “Error opening file” is printed, followed by the system’s description for theerrno
value, which wasENOENT
.Diagnostic: The file does not appear to exist.
: This is the output from ourif (errno == ENOENT)
block, confirming our programmatic check worked as expected.- The program then exits with a failure status, as it should.
- Run with an Existing File: Now, let’s try the “happy path.” First, create an empty file.
touch existing_file.txt
Then, run the program again, this time pointing to the file you just created../open_test existing_file.txt
Expected Output (Success Case)
Attempting to open file: existing_file.txt
File opened successfully. File descriptor is: 3
File closed successfully.
Here, open()
returned a valid file descriptor (likely 3, since 0, 1, and 2 are reserved for stdin
, stdout
, and stderr
). The if (fd == -1)
block was skipped, and the program proceeded to a successful close and exit.
Example 2: Handling a Permissions Error
Next, we will demonstrate the EACCES
error by creating a file that our program is not allowed to read.
Configuration and Execution Steps
1. Create a Protected File: We will create a file and then use the chmod
command to remove its read permissions for everyone.
touch protected_file.txt chmod 000 protected_file.txt
The chmod 000
command revokes all read, write, and execute permissions for the owner, group, and others.
Run the Test Program: Use the same compiled open_test
executable from Example 1, but point it at our new protected file.
./open_test protected_file.txt
Expected Output and Analysis
Attempting to open file: protected_file.txt
Error opening file: Permission denied
Diagnostic: Permission denied. Cannot read the file.
The output is similar to our first test, but the crucial difference is the message. This time, perror()
reports “Permission denied,” which corresponds to the errno
value EACCES
. Our specific check for EACCES
also triggered, printing the corresponding diagnostic. This demonstrates how checking errno
allows your program to distinguish between different failure modes and potentially react to them in different ways.
Tip: After you are finished with this example, you can restore the file’s permissions with
chmod 644 protected_file.txt
or simply remove it withrm protected_file.txt
.
Example 3: A Robust File-Writing Wrapper Function
In professional code, you often encapsulate operations in helper or “wrapper” functions to make the main logic cleaner and ensure error handling is applied consistently. Let’s create a C program with a function that safely writes a string to a file.
File Structure and Code
Create a new file named write_wrapper.c
.
nano write_wrapper.c
Enter the following code. This program defines a function, write_string_to_file
, which handles opening, writing, and closing a file, with robust error checks at every step.
// write_wrapper.c: Demonstrates a robust wrapper function for writing to a file.
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // For strlen()
#include <fcntl.h> // For open() and flags
#include <unistd.h> // For write(), close()
#include <errno.h>
// A wrapper function to demonstrate robust, multi-step error handling.
// Returns 0 on success, -1 on failure.
int write_string_to_file(const char *filepath, const char *data) {
int fd = -1;
ssize_t bytes_to_write;
ssize_t bytes_written;
bytes_to_write = strlen(data);
if (bytes_to_write == 0) {
printf("Warning: Data to write is empty.\n");
// Not necessarily an error, but good to note.
}
// Open the file for writing. Create it if it doesn't exist. Truncate it if it does.
// Set permissions to 0644 (owner can read/write, group/others can read).
fd = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("Failed to open file for writing");
return -1; // Failure
}
// Now, attempt to write the data.
bytes_written = write(fd, data, bytes_to_write);
if (bytes_written == -1) {
perror("Failed to write data to file");
// Cleanup: attempt to close the file descriptor even on write failure.
close(fd);
return -1; // Failure
}
// A subtle but important check: did write() write fewer bytes than requested?
// This can happen if the disk fills up, for example.
if (bytes_written < bytes_to_write) {
fprintf(stderr, "Error: Incomplete write. Wrote %ld of %ld bytes.\n", bytes_written, bytes_to_write);
close(fd);
return -1; // Failure
}
// Finally, close the file.
if (close(fd) == -1) {
perror("Failed to close file after writing");
return -1; // Failure
}
printf("Successfully wrote %ld bytes to %s\n", bytes_written, filepath);
return 0; // Success!
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <filename> \"<text to write>\"\n", argv[0]);
exit(EXIT_FAILURE);
}
char *filepath = argv[1];
char *text = argv[2];
printf("--- Calling write_string_to_file ---\n");
int result = write_string_to_file(filepath, text);
printf("--- Returned from write_string_to_file ---\n");
if (result == 0) {
printf("Main: Operation completed successfully.\n");
return EXIT_SUCCESS;
} else {
fprintf(stderr, "Main: Operation failed.\n");
return EXIT_FAILURE;
}
}
Build, Run, and Verify
1. Compile the Code:
gcc write_wrapper.c -o write_wrapper
2. Run a Successful Test:
./write_wrapper output.txt "Hello Embedded Linux!"
Expected Output:
--- Calling write_string_to_file ---
Successfully wrote 21 bytes to output.txt
--- Returned from write_string_to_file ---
Main: Operation completed successfully.
3. Verify the File Contents: Use the cat
command to check that the file was written correctly.
cat output.txt
You should see: Hello Embedded Linux!
4. Run a Failing Test (Permission Denied): Let’s try to write into a directory that we don’t have permissions for, like the root directory (/
).
./write_wrapper /new_file.txt "This will fail"
Expected Output (Failure):
--- Calling write_string_to_file ---
Failed to open file for writing: Permission denied
--- Returned from write_string_to_file ---
Main: Operation failed.
This example showcases a more realistic development pattern. The main
function’s logic is clean and high-level, delegating the messy details of I/O and error checking to a dedicated, reusable function. Each step within the wrapper function—open
, write
, close
—is followed by a mandatory error check, ensuring the operation is aborted safely at the first sign of trouble.
Common Mistakes & Troubleshooting
Even with an understanding of errno
and return codes, developers new to system programming often fall into a few common traps. Being aware of these pitfalls can save you hours of frustrating debugging.
Exercises
These exercises are designed to be completed on your Raspberry Pi 5. They will reinforce the concepts of checking return codes and using errno
to diagnose specific failures.
- Memory Allocation Failure:
- Objective: Write a program that demonstrates how to handle a memory allocation failure from
malloc()
. - Guidance:
malloc()
returnsNULL
on failure and setserrno
toENOMEM
(“Error, No Memory”). Write a C program that attempts to allocate an impossibly large amount of memory (e.g., 20 gigabytes). - Steps:
- Inside
main
, declare a pointer (e.g.,char *huge_buffer;
). - Call
malloc()
with a very large size:malloc(20L * 1024 * 1024 * 1024);
. TheL
makes the constant along
to avoid overflow. - Check if the returned pointer is
NULL
. - If it is
NULL
, useperror()
to print a descriptive message. - The program should exit gracefully with a failure status. If allocation succeeds (highly unlikely), it should
free()
the memory and report success.
- Inside
- Verification: When you run the program, it should print an error message to
stderr
that includes the text “Cannot allocate memory” and exit.
- Objective: Write a program that demonstrates how to handle a memory allocation failure from
- Filesystem Error Exploration:
- Objective: Modify the
open_test.c
program to specifically identify and report on at least three more error types besidesENOENT
andEACCES
. - Guidance: Use the
man 2 open
command to see a full list of possibleerrno
values for theopen()
system call. Interesting ones to test includeEISDIR
(Is a directory),ENOTDIR
(Not a directory), andEROFS
(Read-only file system). - Steps:
- Copy
open_test.c
to a new file,open_explorer.c
. - Add
else if
clauses to the error handling block to check forEISDIR
,ENOTDIR
, andEROFS
. - For each case, print a custom diagnostic message explaining what that error means.
- To test
EISDIR
, run./open_explorer /home/pi
. - To test
ENOTDIR
, run./open_explorer /home/pi/not_a_dir/some_file
. - Testing
EROFS
is more advanced, but you could try to open a file for writing in a system directory like/proc/
.
- Copy
- Verification: Your program should correctly identify and report the specific reason for failure in each test case.
- Objective: Modify the
- A Safe
strdup
Implementation:- Objective: The standard C library has a
strdup()
function that duplicates a string, but it’s a POSIX extension, not standard C. It works by callingmalloc()
andstrcpy()
. Write your own version,safe_strdup()
, that includes proper error handling. - Guidance: Your function should take one argument (
const char *s
) and return achar *
. It needs to allocate memory, and if the allocation succeeds, copy the string. - Steps:
- Calculate the required memory:
strlen(s) + 1
(for the null terminator). - Call
malloc()
to allocate that much memory. - Crucially, check if
malloc()
returnedNULL
. If so, yoursafe_strdup
function should immediately returnNULL
to signal the failure to its caller.errno
will have already been set bymalloc
. - If allocation succeeds, use
strcpy()
to copy the source string into your new buffer. - Return the pointer to the new buffer.
- Write a
main
function to test yoursafe_strdup
, including a call that themain
function can check forNULL
.
- Calculate the required memory:
- Verification: Your test program should be able to duplicate a string successfully. You can’t easily force
malloc
to fail here, but the important part is having theif (ptr == NULL)
check in your implementation.
- Objective: The standard C library has a
- Error-Checking
cat
Utility:- Objective: Write a simple version of the
cat
command-line utility that reads a file specified on the command line and prints its contents to standard output. It must include robust error handling for every step. - Guidance: The program flow is: open file, loop with
read()
until end-of-file, thenclose()
. Every function call (open
,read
,write
,close
) must be checked. - Steps:
- Check
argc
to ensure a filename was provided. - Call
open()
and check for -1. Useperror()
on failure. - Create a loop. Inside the loop, call
read()
into a buffer (e.g.,char buffer[4096];
). - After
read()
, check the return value (ssize_t bytes_read
). - If
bytes_read
is -1, an error occurred. Callperror()
, close the file, and exit. - If
bytes_read
is 0, you’ve reached the end of the file. Break the loop. - If
bytes_read
> 0, callwrite()
to print the buffer’s contents to standard output (STDOUT_FILENO
, which is file descriptor 1). Check the return ofwrite()
for -1. - After the loop, call
close()
and check its return value.
- Check
- Verification: Your program should successfully print the contents of a text file. It should also report meaningful errors if you give it a non-existent file or a file you don’t have permission to read.
- Objective: Write a simple version of the
Summary
This chapter established the fundamental patterns for robust error handling in C programs on Embedded Linux. By adhering to these practices, you can build applications that are significantly more reliable, stable, and easier to debug.
- Error Indication is a Contract: System and library functions use their return value (
-1
for integers,NULL
for pointers) as the primary signal that an operation has failed. - Always Check Return Values: Ignoring the return value of a function that can fail is a critical programming error that leads to unpredictable behavior and difficult bugs.
errno
Provides the Details: When a function signals an error, it also sets the global, thread-local integererrno
to a code that specifies the reason for the failure.- Check
errno
Conditionally: The value oferrno
is only meaningful immediately after a function has indicated failure. The correct pattern is to check the return value first, then checkerrno
. - Use
perror
andstrerror
for Diagnostics: These functions translate the integererrno
codes into human-readable strings, which are essential for creating useful log and console messages. - Report Errors to
stderr
: Diagnostic messages should always be sent to the standard error stream (stderr
), not standard output (stdout
), to separate normal program output from error reporting. - Discipline is Key: Building robust systems requires the discipline to apply these error-checking patterns consistently to every relevant function call.
Further Reading
For a deeper and more authoritative understanding of the concepts discussed in this chapter, consult the following resources.
- The GNU C Library (glibc) Manual: The official documentation for the C library used on most Linux systems. The section on “Error Reporting” is particularly relevant.
- Linux
man-pages
: The official manual pages are an indispensable resource. Use theman
command on your Raspberry Pi (e.g.,man 2 open
,man 3 perror
,man 3 strerror
,man errno
) for the most precise information.- A web-based version can be found at: https://man7.org/linux/man-pages/
- The Single UNIX Specification (POSIX.1-2017): This is the official standard that defines the behavior of
errno
,perror
, and the system calls we have discussed. It is the definitive source for portable programming. - “The Linux Programming Interface” by Michael Kerrisk: This book is widely regarded as the definitive guide to Linux system programming. Its chapters on file I/O and error handling are exceptionally detailed.
- “Advanced Programming in the UNIX Environment” by W. Richard Stevens and Stephen A. Rago: A classic and foundational text on UNIX/Linux system programming that covers error handling with great clarity.
- Robert C. Seacord, “The CERT C Coding Standard, Second Edition”: This book provides prescriptive rules for secure and reliable C programming, with extensive coverage of error handling rules (see the “ERR” chapter).
- Beej’s Guide to Network Programming: While focused on networking, this guide has one of the clearest and most practical introductions to checking for errors in a C/Linux environment.