C Error Handling with errno, perror, and strerror
C does not provide exceptions like many higher-level languages. Instead, functions commonly report errors through return values, special sentinel values, and sometimes the errno mechanism. The errno macro, perror function, and strerror function form an important part of traditional C error reporting.
What Is errno?
errno is a macro defined by errno.h that provides access to an integer associated with the current thread and used by many C library and POSIX functions to report additional information about an error.
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *end;
long value;
errno = 0;
value = strtol("not-a-number", &end, 10);
if (errno != 0)
{
printf("errno = %d\n", errno);
}
(void)value;
return 0;
}
errno Is Not Automatically Reset
A common misconception is that errno is zero whenever an operation succeeds. That is not generally guaranteed. A function that succeeds may leave errno unchanged.
Therefore, do not inspect errno after every successful function call and assume a nonzero value means that the most recent operation failed. First determine whether the function actually reported an error.
The Correct Error-Handling Pattern
Usually, check the function's documented failure return value first. Only then inspect errno if the function specifies that errno contains useful error information.
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *file = fopen("missing.txt", "r");
if (file == NULL)
{
perror("fopen");
return EXIT_FAILURE;
}
fclose(file);
return EXIT_SUCCESS;
}
Using perror
perror prints a descriptive message associated with the current value of errno. If you provide a prefix, it is printed before the system error description.
#include <stdio.h>
FILE *file = fopen("config.txt", "r");
if (file == NULL)
{
perror("config.txt");
}
A typical diagnostic might look like config.txt: No such file or directory. The exact wording is implementation- and platform-dependent.
Using strerror
strerror converts an error number into a pointer to a human-readable error message.
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
FILE *file = fopen("missing.txt", "r");
if (file == NULL)
{
int errorCode = errno;
printf("Error %d: %s\n", errorCode, strerror(errorCode));
}
return 0;
}
perror vs strerror
| Function | Purpose |
|---|---|
| perror | Prints a message based on the current errno value |
| strerror | Returns a string describing a specified error number |
| errno | Provides access to the error number associated with the relevant failure |
Save errno Before Calling Other Functions
If you need to use errno later, save its value immediately after detecting the failure. Subsequent library calls may change errno.
#include <errno.h>
#include <stdio.h>
#include <string.h>
FILE *file = fopen("missing.txt", "r");
if (file == NULL)
{
int errorCode = errno;
printf("Could not open file: %s\n",
strerror(errorCode));
}
Saving the value is especially useful when error handling performs additional operations before the diagnostic is generated.
Checking fopen Errors
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *file = fopen("data.txt", "r");
if (file == NULL)
{
perror("Unable to open data.txt");
return EXIT_FAILURE;
}
printf("File opened successfully.\n");
fclose(file);
return EXIT_SUCCESS;
}
Checking fclose Errors
Some output errors may only become visible when a stream is flushed or closed. Therefore, programs that care about reliable output should consider the return value of fclose.
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *file = fopen("output.txt", "w");
if (file == NULL)
{
perror("fopen");
return EXIT_FAILURE;
}
fprintf(file, "Hello\n");
if (fclose(file) == EOF)
{
perror("fclose");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Checking malloc Failures
malloc indicates allocation failure by returning NULL. Programs should check that return value before using the allocated memory.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
size_t count = 1000000;
int *values = malloc(count * sizeof *values);
if (values == NULL)
{
perror("malloc");
return EXIT_FAILURE;
}
values[0] = 42;
free(values);
return EXIT_SUCCESS;
}
For malloc, checking the NULL return value is the important step. errno is not the mechanism you should use to determine whether malloc succeeded.
errno with strtol
Some functions use errno as part of their documented error-reporting mechanism. strtol is a useful example because it can indicate range errors through ERANGE.
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const char *text = "999999999999999999999999";
char *end;
errno = 0;
long value = strtol(text, &end, 10);
if (errno == ERANGE)
{
printf("Number is outside the range of long.\n");
return 1;
}
if (end == text || *end != '\0')
{
printf("Invalid number.\n");
return 1;
}
printf("Value: %ld\n", value);
return 0;
}
Why Set errno to Zero Before strtol?
Because errno may contain an old value from an earlier operation, setting errno to zero before a call makes it possible to reliably detect an ERANGE condition reported by that call.
Standard Error Constants
The errno.h header provides symbolic error constants. Common examples include EINVAL for an invalid argument, ENOMEM for insufficient memory in APIs that report it this way, ENOENT for a missing path on POSIX systems, and ERANGE for a range error.
#include <errno.h>
#include <stdio.h>
printf("EINVAL = %d\n", EINVAL);
printf("ERANGE = %d\n", ERANGE);
Not every error constant exists on every C implementation. POSIX systems provide a much larger set of errno values than strictly portable ISO C code can assume.
Comparing errno Values
Compare errno against symbolic constants rather than hard-coded numbers.
#include <errno.h>
if (errno == EINVAL)
{
/* Handle invalid argument. */
}
perror Automatically Uses errno
perror reads the current errno value at the point where it is called. If you need the original failure code after other operations, save errno first and use strerror with the saved value.
Building a Reusable Error Helper
#include <errno.h>
#include <stdio.h>
#include <string.h>
static void reportError(const char *operation)
{
int errorCode = errno;
fprintf(stderr, "%s failed: %s\n",
operation, strerror(errorCode));
}
This pattern is useful when an application wants consistent error messages instead of calling perror throughout the codebase.
Return Values Are Often More Important Than errno
C functions use many different failure conventions. For example, fopen returns NULL, malloc returns NULL, many POSIX system calls return -1, and string conversion functions have their own rules.
| Function | Typical Failure Indicator | Error Information |
|---|---|---|
| fopen | NULL | errno |
| malloc | NULL | Check documented behavior; do not rely on errno |
| strtol | Range/parse conditions | errno plus end-pointer checks |
| POSIX read/write | -1 | errno |
Do Not Use errno Without Checking Failure
FILE *file = fopen("data.txt", "r");
if (file == NULL)
{
perror("fopen");
}
/* Do not conclude that an error occurred merely because
errno happens to be nonzero after a successful operation. */
Error Handling with POSIX System Calls
On POSIX systems, many system calls report failure by returning -1 and setting errno.
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int fd = open("missing.txt", O_RDONLY);
if (fd == -1)
{
perror("open");
return 1;
}
close(fd);
return 0;
}
This is POSIX-specific code rather than strictly portable ISO C because open and O_RDONLY are provided by the POSIX environment.
Handling Errors Without Losing Context
Good error messages explain both what operation failed and why. Include relevant context such as a filename, operation name, or user-provided value.
#include <stdio.h>
const char *filename = "config.ini";
FILE *file = fopen(filename, "r");
if (file == NULL)
{
perror(filename);
}
Returning Errors from Helper Functions
A helper function can return a status code while allowing the caller to decide how the error should be presented.
#include <stdio.h>
int loadConfig(const char *filename)
{
FILE *file = fopen(filename, "r");
if (file == NULL)
{
return -1;
}
fclose(file);
return 0;
}
int main(void)
{
if (loadConfig("config.ini") != 0)
{
perror("loadConfig");
return 1;
}
return 0;
}
When designing such helpers, be careful: if the caller expects errno to describe the underlying failure, the helper should preserve or intentionally set errno according to its documented contract.
Preserving errno in a Helper
#include <errno.h>
#include <stdio.h>
int openConfig(const char *filename)
{
FILE *file = fopen(filename, "r");
if (file == NULL)
{
/* fopen has already established errno. */
return -1;
}
fclose(file);
return 0;
}
int main(void)
{
if (openConfig("config.ini") == -1)
{
int errorCode = errno;
fprintf(stderr, "Configuration error: %s\n",
strerror(errorCode));
return 1;
}
return 0;
}
If the helper performs other operations after the failure, save errno before those operations or explicitly preserve it.
strerror and Thread Safety
The basic strerror interface returns a pointer to an error message and implementations may use shared storage for that message. If thread-safe error-string handling is required, use the facilities provided by the target platform, such as strerror_r where available, while accounting for its differing implementations and semantics.
errno and Threads
In conforming modern C environments, errno behaves as though it provides separate error state for each thread. This means one thread's errno value does not ordinarily overwrite another thread's errno. This does not make the underlying operation itself thread-safe.
errno Is Not a Global Error Log
errno should be viewed as error information associated with particular operations, not as a persistent application-wide record of the last error that happened anywhere in the program.
Common Mistakes
- Checking errno without first checking whether the function reported failure
- Assuming errno is reset to zero after successful operations
- Using hard-coded error numbers instead of symbolic constants
- Forgetting to save errno before performing additional error-handling work
- Assuming every C library function reports errors through errno
- Using errno as a replacement for a function's documented return-value check
- Assuming strerror has identical thread-safety behavior on every platform
Best Practices
- Always follow the documented failure convention of the function
- Inspect errno only when the failed operation specifies that it provides useful errno information
- Use perror for simple diagnostics
- Use strerror when the error message needs to be incorporated into custom output
- Save errno immediately when the value must survive additional operations
- Use symbolic constants such as EINVAL and ERANGE
- Include useful context in error messages
- Return meaningful status values from helper functions
Quick Reference
| Tool | Use It For |
|---|---|
| errno | Inspect an error code associated with a failed operation |
| perror | Print a concise diagnostic using the current errno |
| strerror | Convert an error code into a human-readable message |
| EXIT_FAILURE | Return a conventional failure status from main |
Practice Exercises
- Write a program that opens a nonexistent file and reports the error with perror
- Rewrite the same program using strerror
- Save errno before printing additional diagnostic information
- Use strtol and ERANGE to validate a large integer
- Create a helper function that preserves the underlying errno value
- Build a file-copy utility that reports which operation failed
- Compare the error-handling conventions of fopen, malloc, strtol, and a POSIX system call
Conclusion
Reliable C error handling starts with understanding each function's documented failure mechanism. errno provides an error code for many operations, perror provides a convenient diagnostic, and strerror lets you construct custom messages. The most important habit is to check the function's return value first, then use errno only when the API says it is meaningful.