The System You Have Been Using All Along

C has no exceptions. Every failure travels back to the caller through a return value, and the caller has to check it. There is no second channel, nothing that unwinds the stack on your behalf, and nothing that stops a program carrying on with a value that was never produced. That is the whole of C's error handling, and you have been using it since chapter 2. This lesson does not introduce a system; it names the one you already have, states the rules it runs on, and fills in the three pieces that were missing: where diagnostics go, how the library explains itself, and how to parse a number strictly. Here is the catalogue of what you have already met, and it is short because there is not much to it.

  • scanf returns the number of conversions it completed, so scanf("%d", &value) != 1 is how chapter 2 knew a number had not arrived.
  • fgets returns the buffer, or NULL when it stored nothing, which is why chapter 5's read loop is while (fgets(line, sizeof(line), stdin) != NULL).
  • malloc returns the block, or NULL when it cannot supply one, and chapter 4 made checking it a rule rather than a courtesy.
  • make_book returns the struct it built, or NULL having freed what it already held, which is chapter 6's own function inheriting the library's convention.
  • main returns the exit status, 0 for success and nonzero for failure, which is the same report made one level out, to whatever ran your program.

Two shapes cover all five. A function whose result is a pointer can report failure with a sentinel, a value that could never be a real answer: NULL for a pointer, or -1 where a count or an index is returned. A function whose result is anything else returns a status and writes the real result through a pointer parameter, exactly as scanf returns a count and writes the number into &value. This course's convention is the status with the result delivered through an out-parameter, and a sentinel where the result is a pointer. Which of the two you pick matters far less than holding to it, because mixing conventions across a program is what makes C error handling feel arbitrary rather than mechanical: no caller should have to look up whether this particular function reports trouble with 0, with -1, or with a negated code. The rule that goes with the convention has been in force since chapter 4 and now covers everything: check the return value of every allocation and every I/O call. Not the ones that look risky. Every one.

Diagnostics Belong on Standard Error

Everything you have printed so far went to standard output with printf. There is a second stream, standard error, and fprintf(stderr, "text\n") writes to it: the same function you know with a leading argument naming the stream, since printf(...) is exactly fprintf(stdout, ...). The split is about purpose, not importance. Standard output is the program's output, the thing whoever ran it asked for, which is why ./program > results.txt and ./program | sort work at all. Standard error is the program's commentary about itself. Print a diagnostic to stdout and you have written it into the results, where the next program in the pipe will dutifully try to sort it. There is a second reason, and it is the one that settles the matter when a program dies. When standard output is not a terminal it is fully buffered, so text printed into a pipe or a file sits in memory until the buffer fills or the program exits normally. Standard error is not fully buffered, and its text leaves at once. The next program is wrong on purpose, in its last read.

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

int main(void)
{
    int index = 0;

    if (scanf("%d", &index) != 1 || index < 0)
    {
        fprintf(stderr, "expected an index\n");
        return 1;
    }

    int *values = calloc(4, sizeof *values);

    if (values == NULL)
    {
        fprintf(stderr, "could not allocate\n");
        return 1;
    }

    printf("reading element %d of 4\n", index);
    fprintf(stderr, "reading element %d of 4\n", index);
    printf("element %d is %d\n", index, values[index]);

    free(values);
    return 0;
}

Given 2 it prints reading element 2 of 4 and element 2 is 0 and exits cleanly. Given 4 it reads one past the end of a four element block, which is undefined behaviour, and AddressSanitizer ends the program on the spot. Run that with standard output going into a pipe and the pipe receives nothing at all: not the diagnostic, which never went there, and not reading element 4 of 4, which was printed successfully and then died in the buffer. Standard error meanwhile carries the identical sentence, followed by the report:

reading element 4 of 4
==20==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xfc1f971e0020 at pc 0x000000400d5c

Two calls printed the same text one line apart and only one of them was ever seen. That is the argument in full: the moment you most want a diagnostic is the moment the program is least likely to reach a clean exit. One platform note follows from this and applies to every exercise from here on. This platform compares your standard output against the expected output, so a diagnostic printed with printf becomes part of what is compared and fails a program that is otherwise correct, while the same text sent to stderr is ignored by the check and still visible to you. Errors on standard error is good C everywhere, and here it is also the difference between passing and failing.

errno, and the Two Rules That Make It Usable

A return value can say that a call failed. It usually cannot say why, because there is only one of it and it is already carrying the result. errno is where the library puts the reason: an int declared in <errno.h>, set by a failing library call to a code naming the failure, such as ERANGE for a result too large for its type. It comes with two rules, and neither is optional.

Rule one: errno is meaningful only after a call that documents setting it has already reported failure through its return value. Check the return value first, read errno second, and only then. Nothing in C clears errno on success, so whatever is sitting in it may have been left there by an earlier call that failed, quite possibly one inside the library that your code never made directly. A nonzero errno after a call that succeeded means nothing at all. Rule two: set errno = 0 immediately before a call whose failure is signalled only through errno. Most functions report failure in their return value and use errno merely to elaborate. A few have no spare return value to report with, and strtol is the one you are about to meet: every long it could hand back is also a legitimate answer, so it reports overflow by setting errno to ERANGE and in no other way. Clearing errno first is what turns "errno is ERANGE" into "this call set it to ERANGE".

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

int main(void)
{
    char *end = NULL;

    errno = 0;
    long overflowed = strtol("999999999999999999999999", &end, 10);
    printf("returned %ld with errno %d, %s\n", overflowed, errno, strerror(errno));

    long small = strtol("7", &end, 10);
    printf("returned %ld with errno %d, %s\n", small, errno, strerror(errno));

    return 0;
}
returned 9223372036854775807 with errno 34, Numerical result out of range
returned 7 with errno 34, Numerical result out of range

The first line is rule two earning its keep: twenty four digits do not fit in a long, the return is LONG_MAX, and because errno was cleared beforehand the 34 can only have come from this call. The second line is rule one's trap in the open. strtol("7", &end, 10) succeeded completely and returned 7, and it left errno exactly as it found it, because succeeding is not something a C function reports by resetting anything. Code that read errno at that point and concluded the parse had failed would be wrong about a call that worked perfectly. Notice what is printed alongside the number, too: render the message, do not print the code. 34 tells a reader nothing, while strerror(errno) from <string.h> returns the text for the current value and drops into any format string you like. When you have nothing to add beyond naming what you were attempting, perror("context") is the shorter spelling, printing context: message in one call, straight to standard error where it belongs.

strtol Tells You Where It Stopped

Chapter 5 paired fgets with sscanf, whose returned count says how many conversions happened. strtol is the other parser, and where sscanf says how many, strtol says how far. Read long strtol(const char *text, char **end, int base) as three things: the string to convert, the address of a char * that strtol will aim at the first character it did not consume, and the base, which is 10 for ordinary decimal, 16 for hexadecimal, or 0 to detect the base from a leading 0x or 0. It skips leading whitespace, converts as far as it can, and stops. Everything you need in order to judge the outcome is in end and in errno, and two of the three cases below are invisible in the return value alone. That is the case against atoi, the older function this course has never used: atoi("0") and atoi("oops") both return 0 and nothing distinguishes them.

  • end == text means it converted nothing, because there were no digits where it started. strtol("oops", &end, 10) returns 0 with end still on the o.
  • *end != '\0' means it converted a prefix and stopped at trailing junk. strtol("42abc", &end, 10) returns 42 with end on the a, and strtol("3.5", &end, 10) returns 3 with end on the ..
  • errno == ERANGE means the digits were fine but the value does not fit, and the return is LONG_MAX or LONG_MIN. The whole discipline is short enough to write once and call everywhere.
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

static int parse_long(const char *text, long *out)
{
    char *end = NULL;

    errno = 0;
    long value = strtol(text, &end, 10);

    if (end == text)
    {
        fprintf(stderr, "[%s]: no digits here\n", text);
        return 0;
    }

    if (*end != '\0')
    {
        fprintf(stderr, "[%s]: trailing junk [%s]\n", text, end);
        return 0;
    }

    if (errno == ERANGE)
    {
        perror(text);
        return 0;
    }

    *out = value;
    return 1;
}

int main(void)
{
    const char *inputs[] = {"42", "-17", "oops", "42abc", "999999999999999999999999"};
    long value = 0;

    for (size_t i = 0; i < sizeof inputs / sizeof inputs[0]; ++i)
    {
        if (parse_long(inputs[i], &value))
        {
            printf("parsed %ld\n", value);
        }
    }

    return 0;
}

Standard output holds parsed 42 and parsed -17, the two results the program was asked for and nothing else. The three complaints go to standard error, each named after the input that caused it, and the last of them is what perror produces:

[oops]: no digits here
[42abc]: trailing junk [abc]
999999999999999999999999: Numerical result out of range

parse_long is this course's convention with nothing left implicit. It returns a status, 1 for success and 0 for failure, and delivers the value through out, which it writes only on success, so a caller that forgets to check at least never finds a variable half updated by a call that gave up. The order of the checks is deliberate. end == text comes first, because when nothing was converted there is nothing else worth saying; *end comes next, since a prefix parse is a different complaint from no parse at all; and errno comes last, in keeping with rule one, because by then the call is known to have consumed the entire string and the only remaining question is whether the value fitted.

One Way Out

The more failures you detect, the more exits your function has, and every exit has to release everything acquired so far. That is the tension chapter 4 resolved a chapter before this lesson gave it a name: a function holding two blocks, both initialized to NULL at the top, with each error path jumping forward to a single cleanup: label that frees in reverse order and returns a sentinel. Read that pattern again in failure-handling terms and it is not really a trick about goto; it is the only way to have one place in a function where a free is written and one place to check when you read it back. make_book in chapter 6 is the same idea at smaller scale, freeing the struct it had already allocated before returning NULL when the second allocation failed. Both exist because C has no destructor to run on the way out and no exception to unwind, which is the same absence this lesson opened with. The next lesson adds assert, which is not failure handling at all but a way of stating what you believe can never happen.

Key Takeaways

  • C has no exceptions. Every failure comes back as a return value the caller must check, and the catalogue is the whole language: scanf's count, fgets's NULL, malloc's NULL, make_book's NULL, main's exit status. Check the return value of every allocation and every I/O call.
  • The convention to hold to: a status returned with the result written through an out-parameter, or a sentinel such as NULL or -1 where the result is a pointer or a count. Consistency across a program matters more than which form you pick, and since every failure you detect adds an exit, keep one cleanup path that releases everything acquired: pointers NULL at the top, forward jumps to a single label, frees in reverse order.
  • Diagnostics go to standard error with fprintf(stderr, ...). Standard output is the program's results, redirected and piped as such; standard error is its commentary. Standard output is also fully buffered when piped, so a program that dies loses what it printed there while standard error survives. On this platform expected_output compares standard output only, so a printf diagnostic fails an otherwise correct exercise.
  • errno from <errno.h> is meaningful only after a call that documents setting it has already reported failure. Nothing clears it on success, so a nonzero errno after a call that worked means nothing. Set errno = 0 before any call whose failure is signalled only through it, strtol's ERANGE being this lesson's case. Render the message, not the number: strerror(errno) from <string.h> gives the text, and perror("context") prints context: message straight to standard error.
  • strtol(text, &end, 10) reports how far it got. end == text is no digits, *end != '\0' is trailing junk, and errno == ERANGE is overflow with LONG_MAX or LONG_MIN returned. atoi cannot tell "0" from "oops", which is why this course does not use it.