Error handling with errno
Understand how C reports errors through errno and return values — using perror, strerror, and building robust error-handling patterns.
- Explain what errno is and when it is set
- Use perror and strerror to print human-readable error messages
- Apply the check-and-return pattern to library calls
- List the most common errno values for file operations
C does not have exceptions. Errors in the standard library and system calls are reported through return values — a function returns a special value (NULL, -1, or 0) to indicate failure. The reason for the failure is communicated through errno, a global integer set by library functions when they fail.
What is errno?
errno is declared in <errno.h> as a thread-local integer (on modern systems). When a library function fails, it sets errno to a code that identifies why:
#include <stdio.h>
#include <errno.h>
int main(void) {
FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
printf("errno = %d\n", errno); /* e.g. 2 */
}
return 0;
}The number 2 maps to ENOENT (Error: NO ENTry), meaning "no such file or directory." Numeric codes are not portable — use the symbolic names from <errno.h>.
Common errno values
| Constant | Value | Meaning |
|---|---|---|
ENOENT | 2 | No such file or directory |
EACCES | 13 | Permission denied |
EEXIST | 17 | File exists (when you tried to create exclusively) |
EISDIR | 21 | Is a directory |
ENOSPC | 28 | No space left on device |
ENOMEM | 12 | Not enough memory (malloc) |
EBADF | 9 | Bad file descriptor |
perror — print the error
perror(prefix) prints prefix: followed by the human-readable description of the current errno:
FILE *fp = fopen("secret.txt", "r");
if (!fp) {
perror("fopen"); /* prints: "fopen: Permission denied" */
return 1;
}Always call perror immediately after the failing call — other calls may reset errno.
strerror — get the error string
strerror(errno) returns a pointer to the human-readable error string without printing it:
#include <string.h>
FILE *fp = fopen("secret.txt", "r");
if (!fp) {
fprintf(stderr, "Error opening file: %s\n", strerror(errno));
}Use strerror when you need to incorporate the error description into a larger message.
A robust error-handling pattern
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int copy_file(const char *src_path, const char *dst_path) {
FILE *src = fopen(src_path, "rb");
if (!src) {
fprintf(stderr, "Cannot open source '%s': %s\n",
src_path, strerror(errno));
return -1;
}
FILE *dst = fopen(dst_path, "wb");
if (!dst) {
fprintf(stderr, "Cannot open destination '%s': %s\n",
dst_path, strerror(errno));
fclose(src);
return -1;
}
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), src)) > 0) {
if (fwrite(buf, 1, n, dst) != n) {
fprintf(stderr, "Write error: %s\n", strerror(errno));
fclose(src);
fclose(dst);
return -1;
}
}
if (ferror(src)) {
fprintf(stderr, "Read error: %s\n", strerror(errno));
}
fclose(src);
fclose(dst);
return 0;
}
int main(void) {
if (copy_file("input.txt", "output.txt") != 0) {
return EXIT_FAILURE;
}
printf("Copy successful\n");
return EXIT_SUCCESS;
}Notice: every exit path closes the open files. When the second fopen fails, we close src before returning.
errno and thread safety
In a multi-threaded program, errno is per-thread (on POSIX systems). Each thread has its own errno — setting it in one thread does not affect another. However, within a single thread, errno can be overwritten by any failing library call, so always save or check it immediately after a failure.
errno is only meaningful after a failure. Functions typically set errno only when they fail. Checking errno after a successful call gives meaningless results. Always check the return value first; only then check errno if the return indicates failure.
Where to go next
Next: the CSV parser lab — you will use fgets, sscanf, and error handling to parse a simple CSV file, applying everything from this module.