← Fundamentals
Guided lesson 7 questions · C only

C return codes and errno

Each library function fails in its own way. Read its failure value first, and errno only after it.

Each C library function has its own way to say that it failed: -1, a null pointer, EOF, a count smaller than asked, or no special value at all. errno says why, but only right after a call that reported failure, because a call that works never sets it back to 0. Test the documented failure value first, then read errno, and give your own functions a status result and an output parameter.

Written for C.

Before you begin

Why this matters

C does not stop you from testing the wrong value, or from reading a reason that an earlier call left behind. Code with these mistakes compiles, passes every test in which nothing fails, and gives a wrong message or a wrong number the first time something does. When you know how each function signals failure, you can write the test that catches it.

What this helps you reason about

  • Name the failure value of fopen, remove, getchar, sscanf, and strtol, and test each one correctly.
  • Explain why errno can name the wrong reason, and say when to set it to 0.
  • Write a function that returns a status code and stores its result only on success.

In Python or Java, a failed call throws an exception that carries a message. In Go and Rust, it returns an error value next to the result. A C library function does neither, and the library functions do not all fail in the same way: remove returns -1, fopen returns a null pointer, getchar returns EOF, and strtol returns a number that looks like any other. Errors as values or exceptions showed the basic idea: a C function reports failure through its return value, and errno says why. This lesson reads each kind of failure value, shows when errno can be trusted, and designs functions of your own that report failure clearly.

Already know this? The short version
  • Each library function has its own failure value: -1 (remove, close), NULL (fopen, malloc), EOF (getchar), a count smaller than asked (sscanf, fread), or none at all (strtol, atoi). Test the one its documentation names.
  • errno is set on failure and never set back to 0, so it can hold an earlier call’s reason: test the result first, set errno = 0 before strtol, and copy errno into a local before any other call, printf included.
  • strtol needs three tests, end != text, *end == '\0', and errno != ERANGE, then a range check on the long before it goes into an int; atoi cannot report failure at all.
  • Your own functions return a status from an enum and write the result through an output parameter only on success. Bugs from real programs are in “What goes wrong with return codes in real programs?”.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
return code
A number a function returns to say whether it worked, such as 0 for success and -1 for failure. The function’s type does not say which numbers mean failure; its documentation does.
null pointer
A pointer that points to nothing, written NULL in code and shown as 0x0 by the debugger. getenv returns NULL for a variable that is not set.
EOF
A negative int constant from <stdio.h>, -1 with glibc, that getchar and other input functions return when there is no more input or a read failed.
errno
A variable, declared in <errno.h>, in which many library functions store a number that names why they failed, such as ERANGE. It means something only right after a call that reported failure.
end pointer
The char * in which strtol stores the address of the first character it did not use. If it equals the start of the text, strtol read no digits.
output parameter
A pointer parameter through which a function stores a result, so that its return value is free for a return code.
enum
A type that names a list of int constants, such as enum Kind { WEIGHED, COUNTED };, where WEIGHED is 0 and COUNTED is 1.
short count
A count result that is smaller than the number of items the call asked for, such as sscanf filling 1 of 2 fields.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Failure values

How does each library function say that it failed?

Errors as values or exceptions showed that a C function reports failure through its return value, and that errno, from <errno.h>, says why. This lesson looks at the library functions you call every day. Each has its own failure value, and its documentation says which. On Linux, that is the RETURN VALUE part of the function’s manual page.

  • 0 for success and -1 for failure, with a reason in errno: remove, from <stdio.h>, which deletes a file, and most POSIX functions, such as close.
  • A null pointer for failure: fopen, which opens a file and returns a FILE *, and malloc. A null pointer points to nothing; it is written NULL.
  • EOF for “no more input, or a read failed”: getchar and fgetc. EOF is a negative int from <stdio.h>, -1 with glibc, the C library in the stepper.
  • A count that can be smaller than you asked for: sscanf, fread, and fwrite.
  • No failure value at all: every long that strtol returns can be a real number.

Test each result against its own kind of value. if (remove(path)) is true when remove failed, because 0 means success.

main.c
FILE *cues = fopen("/show/cues.txt", "r");
if (cues == NULL) {
    /* failed: errno says why */
}
if (remove("/show/old-cues.txt") != 0) {
    /* failed: errno says why */
}
int first = getchar();
if (first == EOF) {
    /* no more input, or a read failed */
}

Each C library function has its own failure value; test the one its documentation names.

Coming from Python: open("cues.txt") raises FileNotFoundError, and nothing runs until something catches it. fopen returns NULL, and the next line runs as usual, whether or not it tests the pointer.

Precisely: what ISO C and POSIX each promise

C11 7.21.5.3 says only that fopen returns a null pointer on failure. ISO C does not require fopen to set errno, but POSIX does, and glibc does. C11 7.21.4.1 says remove returns a nonzero value on failure; POSIX says -1. After EOF, feof(stdin) and ferror(stdin) ask the stream which of the two happened. getchar returns an int, not a char, so that EOF differs from every byte value from 0 to 255.

Experiment

Ask three library calls to fail

fopen reports failure with a null pointer, remove with -1, and getchar with EOF. fopen and remove also store a reason in errno. getchar reached the end of the input, which is not an error, so it left errno as it was.

Why it matters Look up the failure value of each function before you test it. After a call that did not fail, errno can hold an old reason.

There is no /show folder, and standard input is empty. Predict the result of fopen, remove, and getchar. Then change the last printf to printf("getchar: %d, errno %d\n", first, errno); and predict the errno it prints.

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

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    FILE *cues = fopen("/show/cues.txt", "r");
    if (cues == NULL) {
        printf("fopen: NULL, errno %d\n", errno);
    }
    int removed = remove("/show/old-cues.txt");
    printf("remove: %d, errno %d\n", removed, errno);
    int first = getchar();
    printf("getchar: %d\n", first);
    return 0;
}
What to look for

After the fopen line, cues shows 0x0: fopen returned a null pointer. errno is not in the variables panel, because it is not an ordinary variable, so the program prints it. The run prints fopen: NULL, errno 2, then remove: -1, errno 2, then getchar: -1. 2 is the code for “no such file or directory”, and -1 is the value of EOF with this C library; removed and first show -1 in the panel. After the edit, the last line is getchar: -1, errno 2. getchar did not fail: the input simply ended. errno still holds the 2 that remove stored.

Expected output

fopen: NULL, errno 2
remove: -1, errno 2
getchar: -1

02 / errno

When does errno hold the right reason?

errno is one int that every library call shares. Its rules are short, and the trap is in them.

A function that fails stores a reason code in errno, such as ENOENT (“no such file or directory”) or ERANGE (“result out of range”). A function that works does not set errno back to 0: no library function ever does. It may leave the old value there, and it may even change it.

So errno can hold a reason from an earlier call. Read errno only after the result has told you that this call failed. For a function whose result cannot show failure, such as strtol, set errno = 0 yourself just before the call, and test errno right after it.

main.c
errno = 0;
long cue = strtol(text, &end, 10);
if (errno == ERANGE) {
    /* too large or too small for a long */
}

errno is set on failure and never cleared on success; read it only right after a call that reported failure, or set it to 0 before strtol.

Coming from Go: A Go function returns its error next to its result, so each error belongs to one call. errno is one variable for the whole thread, and the next call may change it.

Precisely: C11 7.5

7.5p3: errno is zero at program start, no library function sets it to zero, and any library function may set it to a nonzero value even when there is no error, unless its description says otherwise. 7.5p2 makes errno a macro for an int lvalue with thread local storage duration, so each thread has its own. glibc defines it as (*__errno_location ()), a function call, which is why the stepper does not list errno with the local variables. ENOENT and ERANGE are macros, and their numbers differ between systems; ENOENT is 2 and ERANGE is 34 in the stepper’s build. Compare with the names, not the numbers.

Experiment

Read a reason that belongs to another call

fopen stored 2 in errno. strtol worked and did not touch errno, because no library function ever sets errno back to 0. The test read the old value.

Why it matters Set errno to 0 just before a call whose result cannot show failure, such as strtol. For other calls, test the result first, and read errno only when the result says the call failed.

fopen fails, and then strtol reads "42" without any problem. Predict what the program prints. Then add errno = 0; on the line before long cue = strtol("42", &end, 10); and predict again.

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

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    FILE *cues = fopen("/show/cues.txt", "r");
    if (cues != NULL) {
        fclose(cues);
        return 0;
    }
    /* no cue file: read the cue typed on the desk */
    char *end = NULL;
    long cue = strtol("42", &end, 10);
    if (errno != 0) {
        printf("cue refused: %s\n", strerror(errno));
        return 0;
    }
    printf("cue %ld\n", cue);
    return 0;
}
What to look for

cues shows 0x0, so the program skips the first if and goes on to strtol. After the strtol line, cue shows 42, and end points at the empty text after the digits, shown as "": strtol used every character. The next stop is still the printf inside if (errno != 0), and the run prints cue refused: No such file or directory. That reason belongs to fopen. After the edit, the stepper goes from the if to the last printf, and the run prints cue 42.

Expected output

cue refused: No such file or directory

03 / Reading numbers

How do you read a number when every result could be real?

strtol(text, &end, 10), from <stdlib.h>, reads a whole number from text; 10 is the base. Every long it returns can be a real number, so its result alone never shows failure. The read_count function in Errors as values or exceptions made three tests. Here is what each one catches.

  • end: strtol stores the address of the first character it did not use in end, the end pointer. If end equals text, no digit was read, and the result is 0.
  • *end: the character that end points at. If it is not '\0', more text followed the number, as in "12b".
  • errno: if the number does not fit in a long, strtol returns LONG_MAX or LONG_MIN and sets errno to ERANGE. That is why errno = 0 comes first.

strtol skips spaces at the start and accepts a + or - sign, so " +40" gives 40. Check the range your program needs on the long, before you store the number in an int.

atoi(text), also from <stdlib.h>, reads in the same way but returns only an int. atoi("x42") and atoi("0") are both 0, and atoi has no way to report failure. Do not use it on text a person typed.

main.c
char *end = NULL;
errno = 0;
long value = strtol(text, &end, 10);
if (end == text || *end != '\0' || errno == ERANGE) {
    return -1; /* no digits, extra text, or too large */
}
if (value < 1 || value > 999) {
    return -1; /* a number, but not a cue number */
}

strtol needs three tests: end moved, *end is '\0', and errno is not ERANGE. atoi cannot report failure.

Coming from Rust: "x42".parse::<i32>() returns an Err you must handle. atoi("x42") returns 0, and nothing tells you that the text held no number.

Precisely: C11 7.22.1.4 and 7.22.1.2

7.22.1.4: with no digits, strtol returns 0 and stores the start of the text in end; for a value out of range, it returns LONG_MAX or LONG_MIN and stores ERANGE in errno. In the stepper’s build a long is 8 bytes, so ERANGE starts beyond 9223372036854775807. Base 0 reads 0x1F as hexadecimal and 010 as octal 8, which surprises people who type a leading zero; use base 10 for numbers people type. 7.22.1.2: atoi(text) gives the same result as (int)strtol(text, NULL, 10) except for errors, and when the value cannot be represented, the behavior is undefined.

Experiment

Find where strtol stopped reading

strtol reads digits while it can, stores the address of the first character it did not use in end, and returns the number. With no digits at all, it returns 0 and sets end to the start of the text. atoi reads in the same way but returns only the number, so "x42" and "0" give the same result.

Why it matters Accept a typed number only when end moved past the start and points at the closing '\0'. atoi cannot tell you that it found no number.

Predict cue, the text that end points at, and what atoi gives for "4x2". Then change "4x2" to "x42" and predict again.

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

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    const char *typed = "4x2";
    char *end = NULL;
    errno = 0;
    long cue = strtol(typed, &end, 10);
    printf("cue %ld, errno %d, rest \"%s\"\n", cue, errno, end);
    printf("atoi: %d\n", atoi(typed));
    return 0;
}
What to look for

typed and end show an address and the text it points to. After the strtol line, cue shows 4, and end shows an address one higher than typed, pointing at "x2". The run prints cue 4, errno 0, rest "x2", then atoi: 4. After the edit, cue shows 0, end shows the same address as typed, and the run prints cue 0, errno 0, rest "x42", then atoi: 0. Only end shows that no digit was read.

Expected output

cue 4, errno 0, rest "x2"
atoi: 4

04 / Messages

How do you turn errno into a message?

errno is a number. strerror(errno), from <string.h>, returns its text: strerror(ENOENT) is No such file or directory with glibc. perror("open cues"), from <stdio.h>, writes your text, a colon, a space, and that same text to standard error, the second output stream that programs use for messages: open cues: No such file or directory. The stepper shows only standard output, so its experiments print strerror(errno) with printf.

Any call can change errno, including the printf or fclose you write while you handle a failure. Copy errno into a local variable on the line right after the failing call, and use the copy.

main.c
FILE *cues = fopen("/show/cues.txt", "r");
if (cues == NULL) {
    int saved = errno; /* copy it before any other call */
    printf("no cue file: %s\n", strerror(saved));
    return -1;
}

Copy errno at once, then turn the copy into text with strerror or perror.

Coming from Java: An exception object carries its message for as long as you keep it. errno holds one number for the last failure, and you must copy it before the next call.

Precisely: C11 7.24.6.2 and 7.21.10.4

strerror returns a pointer to a string that the program must not change and that a later call to strerror may overwrite. perror writes to stderr. The texts belong to the C library: glibc, musl, and macOS word some of them differently, so never compare them in code. strerror_r, from POSIX, writes the text into a buffer you pass, which is safe when several threads report errors.

05 / Your own codes

How should your own functions report failure?

For your own functions, use the design that take used in Errors as values or exceptions: return a status, and write the result through an output parameter. Two rules make it safe.

Give each kind of failure its own code. A project enum names them: enum cue_status { CUE_OK, CUE_NO_NUMBER, CUE_EXTRA, CUE_RANGE }; makes four int constants, 0 to 3. CUE_OK comes first, so 0 means success, as with remove. A function such as cue_status_text turns a code into a message, as strerror does for errno.

Write the output parameter last, after every test has passed. Then a failed call leaves the caller’s variable as it was, and a caller never receives half a result.

Keep errno for the C library; the next library call may change it. Propagation and unwinding shows how a caller passes such a code up, and Cleanup on a failing path shows how to release what a function took before it returns one.

main.c
enum cue_status { CUE_OK, CUE_NO_NUMBER, CUE_EXTRA, CUE_RANGE };

const char *cue_status_text(enum cue_status status) {
    switch (status) {
    case CUE_OK: return "ok";
    case CUE_NO_NUMBER: return "no number";
    case CUE_EXTRA: return "text after the number";
    case CUE_RANGE: return "not a cue from 1 to 999";
    }
    return "unknown status";
}

Return a status from an enum, write the result through an output parameter last, and keep errno for the library.

Precisely: what the compiler checks

An enum constant has type int (C11 6.7.2.2), so a function declared to return enum cue_status may return any int, and the compiler does not object. With -Wall, gcc warns when a switch on an enum leaves out a constant and has no default; that is why cue_status_text has no default and returns after the switch. __attribute__((warn_unused_result)), from Errors as values or exceptions, makes an ignored status a warning; C23 spells it [[nodiscard]].

Experiment

Return a status and keep the output unchanged

read_cue returns a named constant that says which test failed. It writes through cue only after every test has passed, so the caller always gets a status and gets a number only on success. (int)value converts the long to an int, which is safe once the range test has passed.

Why it matters Give each kind of failure its own enum constant, and write an output parameter last, so that a failed call leaves the caller’s variable as it was.

Predict status and cue after read_cue("12b", &cue). Then change "12b" to "12" and predict again.

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

enum cue_status { CUE_OK, CUE_NO_NUMBER, CUE_EXTRA, CUE_RANGE };

enum cue_status read_cue(const char *text, int *cue) {
    char *end = NULL;
    errno = 0;
    long value = strtol(text, &end, 10);
    if (end == text) {
        return CUE_NO_NUMBER;
    }
    if (*end != '\0') {
        return CUE_EXTRA;
    }
    if (errno == ERANGE || value < 1 || value > 999) {
        return CUE_RANGE;
    }
    *cue = (int)value; /* only on success */
    return CUE_OK;
}

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    int cue = 0;
    enum cue_status status = read_cue("12b", &cue);
    printf("status %d, cue %d\n", status, cue);
    return 0;
}
What to look for

In read_cue, after the strtol line, value shows 12 and end points at "b". The first test is false, the second is true, and the stepper stops on return CUE_EXTRA. The parameter cue shows the address of main’s cue, → 0, and that 0 never changes. Back in main, status shows CUE_EXTRA, the name of the constant, while printf prints its number: status 2, cue 0. After the edit, all three tests are false, *cue = (int)value stores 12, status shows CUE_OK, and the run prints status 0, cue 12.

Expected output

status 2, cue 0

06 / In production

What goes wrong with return codes in real programs?

The rules above explain four bugs that C programmers meet in real programs.

errno read too late. Code calls fopen, sees NULL, writes a log line, closes another file, and then prints strerror(errno). Any of those calls may have changed errno, so the message can name the wrong reason. Copy errno on the line after the failing call.

A short count taken for success. sscanf returns how many fields it filled, and EOF only when the input ended before the first one. A test for EOF alone accepts "19" as a time and keeps the old minute; the experiment below shows it. fread and fwrite also return a count of items, and their documentation says when it can be smaller.

The result of getchar kept in a char. getchar returns an int, so that EOF differs from every byte. char c = getchar(); loses that difference. Where char is signed, as on most x86 computers, the byte 255 looks like EOF. Where char is unsigned, as in the stepper's ARM build, c == EOF is never true, a loop that waits for it never ends, and gcc warns: comparison is always false due to limited range of data type. Keep the result in an int.

A long stored in an int before the range check. int seat = strtol(text, &end, 10); converts at once: in the stepper's build, "4294967298" becomes 2, which passes a check for 1 to 999. Check the range on the long, then convert.

main.c
int c = getchar();       /* int, so EOF stays -1 */
long value = strtol(text, &end, 10);
if (value < 1 || value > 999) { /* range on the long */
    return -1;
}
int seat = (int)value;   /* now it fits */

Copy errno at once, compare counts with what you asked for, keep getchar in an int, and check the range before converting.

Precisely: conversions to a smaller type

C11 6.3.1.3p3: converting a value that does not fit to a signed integer type gives an implementation-defined result or raises a signal. gcc documents that it reduces the value modulo 2 to the power 32 for int, which is why 4294967298 becomes 2. Whether plain char is signed is also implementation-defined (6.2.5p15); on aarch64 Linux it is unsigned, on x86-64 Linux it is signed. -Wtype-limits, part of -Wextra, gives the warning about c == EOF.

Experiment

Accept a time that sscanf read only half of

sscanf returns EOF only when the input ends before the first field. Here it filled hour, found no ":", stopped, and returned 1. minute kept the 0 it started with.

Why it matters Compare a count with the number of items you asked for. A test for EOF alone accepts input that is only half there.

sscanf is asked for two fields, an hour and a minute, and finds one. Predict what prints. Then change filled == EOF to filled != 2 and predict again.

#include <stdio.h>

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    int hour = 0;
    int minute = 0;
    int filled = sscanf("19", "%d:%d", &hour, &minute);
    if (filled == EOF) {
        printf("no curtain time\n");
        return 0;
    }
    printf("curtain at %d:%02d, %d filled\n", hour, minute, filled);
    return 0;
}
What to look for

%02d prints a number with at least two digits, adding a 0 in front. After the sscanf line, hour shows 19, minute still shows 0, and filled shows 1. 1 is not EOF, so the stepper skips the if, and the run prints curtain at 19:00, 1 filled: the minute was never read. After the edit, the if is taken, and the run prints no curtain time.

Expected output

curtain at 19:00, 1 filled

07 / Try it

Can you predict what errno holds?

There is no /show folder. Work out each value before you read the printf lines.

main.c
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    char *end = NULL;
    errno = 0;
    long fade = strtol("30 sec", &end, 10);
    printf("%ld %d [%s]\n", fade, errno, end);
    FILE *fades = fopen("/show/fades.txt", "r");
    long next = strtol("8", &end, 10);
    printf("%ld %d %d\n", next, errno, fades == NULL);
    return 0;
}

What are the two output lines? Would a test of errno after the second strtol accept the 8?

Reveal the reasoning

30 0 [ sec], then 8 2 1. strtol reads 30 and stops at the space, so end points at " sec", and errno is still the 0 set before the call. fopen fails and stores 2, the code for “no such file or directory”, in errno. The second strtol works and does not set errno back to 0, so the line shows 8 with the old 2, and fades == NULL is 1. A test of errno there would refuse a good number: the program needs errno = 0 before the second strtol too.

Practice tickets

  1. C-601 · Good tempos are refused after an earlier failure → Free
  2. C-602 · Transposing down one semitone shows an error → Sign in · Pro
  3. C-603 · Long surtitle lines are cut off, but the log says they were shown → Sign in · Pro

Keep exploring

I/O, buffering, and external state →

Primary references