Why this matters
Files go missing, permissions are wrong, disks fill up, networks drop. A program that assumes every call succeeds will one day crash, or worse, report "saved" while the data is gone. C has no exceptions: nothing stops your program when a call fails. Each function tells you about failure through its return value, and it is entirely up to you to look. This lesson shows the C error conventions, how to find out why something failed, and how to report it the way Unix tools do.
Failure is in the return value
There is no single rule, but the standard library mostly follows a few patterns. You learn each function's convention from its man page, in the section called RETURN VALUE.
| function | success | failure |
|---|---|---|
fopen | a FILE * | NULL |
fgets | the buffer pointer | NULL (end of file or error) |
fgetc, fputc, fputs, fclose, fflush | a non-negative value or 0 | EOF |
fprintf, printf | number of characters written | a negative number |
fscanf, scanf | number of items converted | fewer than requested, or EOF |
malloc | a pointer | NULL |
POSIX calls like open, read (module 15) | a non-negative number | -1 |
The return value answers "did it work?". It usually does not say why not. For that there is errno.
errno: the reason code
<errno.h> declares errno, an int that library functions set to a positive code when they fail. Each code has a name, a macro starting with E:
| name | value on Linux | meaning |
|---|---|---|
ENOENT | 2 | No such file or directory |
EACCES | 13 | Permission denied |
EEXIST | 17 | File exists |
EISDIR | 21 | Is a directory |
ENOSPC | 28 | No space left on device |
EROFS | 30 | Read-only file system |
Compare against the names, never the numbers; the numbers differ between operating systems.
Underneath, these codes come from the kernel. When fopen calls the open system call and the kernel refuses, it returns a negative error number; the C library stores it in errno and returns NULL to you. (errno is per thread, so threads cannot overwrite each other's codes.)
your code C library kernel
fopen("x","r") ---> open("x", O_RDONLY) ---> no such file
errno = ENOENT <--- returns -ENOENT
NULL <---
Two rules make errno reliable:
- Only read it after a call has reported failure. Successful calls are allowed to change
errnoto any value, and none of them resets it to 0. "errno is nonzero" means nothing on its own. - Read it immediately, or copy it into a local variable. Any other library call in between, including
printf, may overwrite it.
Turning codes into words: strerror and perror
strerror(code) (from <string.h>) returns a human-readable description. perror(prefix) prints prefix: description and a newline to stderr, using the current errno. Here is a program that tries several opens and explains each result:
#include <errno.h>
#include <stdio.h>
#include <string.h>
static void try_open(const char *path, const char *mode)
{
FILE *f = fopen(path, mode);
if (f == NULL) {
int err = errno; /* save it before calling anything else */
printf("%-20s %-2s errno=%-2d %s\n", path, mode, err, strerror(err));
return;
}
printf("%-20s %-2s ok\n", path, mode);
fclose(f);
}
int main(void)
{
try_open("notes.txt", "w");
try_open("missing.txt", "r");
try_open("/etc/shadow", "r");
try_open("/usr/notes.txt", "w");
try_open("/work", "w");
try_open("nodir/notes.txt", "w");
return 0;
}
Output in the sandbox:
notes.txt w ok
missing.txt r errno=2 No such file or directory
/etc/shadow r errno=13 Permission denied
/usr/notes.txt w errno=30 Read-only file system
/work w errno=21 Is a directory
nodir/notes.txt w errno=2 No such file or directory
/etc/shadow holds password hashes and is readable only by root. The sandbox's root file system is mounted read-only, so creating anything under /usr fails with EROFS. Note the last line: creating a file in a directory that does not exist reports ENOENT too, because a component of the path is missing.
The conventional Unix error message is program-or-file: reason, which is exactly what perror produces when you pass the file name:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "usage: %s FILE\n", argv[0]);
return EXIT_FAILURE;
}
FILE *f = fopen(argv[1], "r");
if (f == NULL) {
perror(argv[1]);
return EXIT_FAILURE;
}
printf("opened %s\n", argv[1]);
fclose(f);
return EXIT_SUCCESS;
}
$ ./prog missing.txt
missing.txt: No such file or directory
$ echo $?
1
$ ./prog /etc/shadow
/etc/shadow: Permission denied
Compare with cat missing.txt, which prints cat: missing.txt: No such file or directory. Same convention.
Writes fail late: checking fclose
Writing seems like it cannot fail once the file is open, but disks fill up and network file systems disconnect. Because output is buffered, the failure often does not appear in the call that "wrote" the data, but later, when the buffer is actually handed to the kernel. Linux provides a device that makes this easy to see: /dev/full can be opened, but every write to it fails with ENOSPC.
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
FILE *f = fopen("/dev/full", "w");
if (f == NULL) {
perror("fopen");
return 1;
}
int n = fprintf(f, "important data\n");
printf("fprintf returned %d\n", n);
if (fclose(f) == EOF) {
printf("fclose failed: %s\n", strerror(errno));
return 1;
}
printf("saved\n");
return 0;
}
$ ./full
fprintf returned 15
fclose failed: No space left on device
fprintf reported success: it copied 15 bytes into the buffer. The real write happened inside fclose, and that is where the failure surfaced. A program that ignores the return value of fclose would have printed "saved" and exited 0 with nothing saved. For data you care about, check every write and the final fclose (or fflush).
Cleaning up on the error path
When a function fails halfway, release what you already acquired before returning:
FILE *in = fopen("in.txt", "r");
if (in == NULL) {
perror("in.txt");
return 1;
}
FILE *out = fopen("out.txt", "w");
if (out == NULL) {
perror("out.txt");
fclose(in); /* do not leak the first file */
return 1;
}
With more resources this gets repetitive. A common C idiom uses goto to jump to a single cleanup section at the end of the function; you will see it in the dynamic memory and systems modules.
Common mistakes
- Reading
errnowithout a failure.if (errno != 0) { error }after a successful call can fire at random. Check the return value first. - Calling something before
perror.printf("failed\n"); perror("x");may print the wrong reason, becauseprintfcan changeerrno. perrorwith a vague prefix.perror("error")giveserror: No such file or directory. Which file? Pass the file name.- Ignoring
fclose's result after writing, as shown with/dev/full. - Printing the error and exiting 0. Make the exit status nonzero.
- Forgetting
#include <errno.h>(forerrnoand theEnames) or<string.h>(forstrerror). gcc reports'errno' undeclaredor an implicit declaration error.
Recap
- C functions report failure through return values:
NULL,EOF, negative numbers or-1. Read each function's RETURN VALUE section. - After a failure,
errnoholds a code such asENOENTorEACCES; read it immediately, and only after a failure. strerror(errno)gives the text;perror(name)printsname: textto stderr.- Buffered writes fail late: check
fcloseandfflushwhen data matters. - Report errors on stderr, clean up what you acquired, and exit with a nonzero status.
Going further: run man 3 errno for the full list of codes, and errno -l if the moreutils package is installed on your own machine. Try strace -e trace=openat ./prog missing.txt there too: you will see the system call and the ENOENT it returned.