Input, output and files · about 45 min

Errors: return values, errno and perror

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.

functionsuccessfailure
fopena FILE *NULL
fgetsthe buffer pointerNULL (end of file or error)
fgetc, fputc, fputs, fclose, fflusha non-negative value or 0EOF
fprintf, printfnumber of characters writtena negative number
fscanf, scanfnumber of items convertedfewer than requested, or EOF
malloca pointerNULL
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:

namevalue on Linuxmeaning
ENOENT2No such file or directory
EACCES13Permission denied
EEXIST17File exists
EISDIR21Is a directory
ENOSPC28No space left on device
EROFS30Read-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:

  1. Only read it after a call has reported failure. Successful calls are allowed to change errno to any value, and none of them resets it to 0. "errno is nonzero" means nothing on its own.
  2. 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

Recap

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.

Exercises

Exercise 1 · quiz

When errno means something

Which statements about errno are true? Select all that apply.

Answer
  • After a library call fails (for example fopen returns NULL), errno usually holds a code saying why

  • Calling printf between the failure and reading errno can change errno

errno is only meaningful right after a call that reported failure through its return value. Successful calls never promise to reset it to 0, and many successful calls change it as a side effect of internal work (printf might try something, fail, and fall back). So: check the return value first, and only then read errno, immediately, or save it in a local variable before doing anything else.

Exercise 2 · write code

Explain every failure

Write a program that tries to open, for reading, every file named on its command line, in order. For each one, print to stdout exactly one line:

data.txt: ok
missing.txt: No such file or directory
/etc/shadow: Permission denied

that is, the name, a colon, a space, and either ok or the system's description of the error (use strerror(errno)). Close every file you successfully opened.

The exit status is 0 if every file opened, and 1 if at least one failed. With no arguments, print nothing and exit with 0.

File: main.c

Starting point

#include <errno.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    for (int i = 1; i < argc; i++) {
        FILE *f = fopen(argv[i], "r");
        /* report ok or strerror(errno) */
        fclose(f);
    }
    return 0;
}

Example with arguments data.txt missing.txt /etc/shadow

expected output

data.txt: ok
missing.txt: No such file or directory
/etc/shadow: Permission denied

Example with arguments data.txt notes.txt

expected output

data.txt: ok
notes.txt: ok
Hint 1

Check f == NULL before doing anything else with f. Calling fclose(NULL) crashes.

Hint 2

Inside the failure branch, printf("%s: %s\n", argv[i], strerror(errno)); and remember that something failed.

Hint 3

Keep a counter of failures and return failures > 0 ? 1 : 0; at the end.

Reference solution
#include <errno.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int failures = 0;
    for (int i = 1; i < argc; i++) {
        FILE *f = fopen(argv[i], "r");
        if (f == NULL) {
            printf("%s: %s\n", argv[i], strerror(errno));
            failures++;
            continue;
        }
        printf("%s: ok\n", argv[i]);
        fclose(f);
    }
    return failures > 0 ? 1 : 0;
}

Exercise 3 · write code

A save that cannot lie

Write a program that saves the line hello (followed by a newline) into the file named by argv[1], and checks every step that can fail: fopen, fprintf, and fclose.

The checker runs your program with a normal file, with a path in a directory that does not exist, and with /dev/full, a special device on Linux that accepts an open but fails every write with "No space left on device". Think carefully about where that failure shows up when output is buffered.

File: main.c

Starting point

#include <stdio.h>

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "usage: prog FILE\n");
        return 2;
    }
    FILE *f = fopen(argv[1], "w");
    if (f == NULL) {
        perror(argv[1]);
        return 1;
    }
    fprintf(f, "hello\n");
    fclose(f);
    printf("saved %s\n", argv[1]);
    return 0;
}

Example with arguments out.txt

expected output

saved out.txt

Example with arguments no/such/dir/out.txt

expected output

(nothing)
Hint 1

fprintf returns a negative number on failure; fclose returns EOF on failure.

Hint 2

With /dev/full, fprintf succeeds (the text only goes into the buffer). The real write happens when fclose flushes, so that is where the error appears.

Hint 3

if (fclose(f) == EOF) { perror(argv[1]); return 1; }

Reference solution
#include <stdio.h>

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "usage: prog FILE\n");
        return 2;
    }
    FILE *f = fopen(argv[1], "w");
    if (f == NULL) {
        perror(argv[1]);
        return 1;
    }
    if (fprintf(f, "hello\n") < 0) {
        perror(argv[1]);
        fclose(f);
        return 1;
    }
    if (fclose(f) == EOF) {
        perror(argv[1]);
        return 1;
    }
    printf("saved %s\n", argv[1]);
    return 0;
}

Exercise 4 · quiz

Reporting to the caller

A program fails to open its input file. It prints config.txt: No such file or directory to stderr. What should it do next?

Answer
  • Exit with a nonzero status, so scripts and make know it failed

The message is for the human; the exit status is for programs. A script running ./prog && deploy must not deploy after a failure. Continuing with defaults can be the right design, but then it is not an error and the message should say so ("using defaults"). Error text belongs on stderr only, so it does not end up mixed into data on stdout.