Handling Failure
Report failure with return codes, understand errno and perror, check every malloc and I/O call, and keep a single cleanup path that releases everything acquired.
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.
scanfreturns the number of conversions it completed, soscanf("%d", &value) != 1is how chapter 2 knew a number had not arrived.fgetsreturns the buffer, orNULLwhen it stored nothing, which is why chapter 5's read loop iswhile (fgets(line, sizeof(line), stdin) != NULL).mallocreturns the block, orNULLwhen it cannot supply one, and chapter 4 made checking it a rule rather than a courtesy.make_bookreturns the struct it built, orNULLhaving freed what it already held, which is chapter 6's own function inheriting the library's convention.mainreturns 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 == textmeans it converted nothing, because there were no digits where it started.strtol("oops", &end, 10)returns 0 withendstill on theo.*end != '\0'means it converted a prefix and stopped at trailing junk.strtol("42abc", &end, 10)returns 42 withendon thea, andstrtol("3.5", &end, 10)returns 3 withendon the..errno == ERANGEmeans the digits were fine but the value does not fit, and the return isLONG_MAXorLONG_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'sNULL,malloc'sNULL,make_book'sNULL,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
NULLor-1where 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: pointersNULLat 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 platformexpected_outputcompares standard output only, so aprintfdiagnostic fails an otherwise correct exercise. errnofrom<errno.h>is meaningful only after a call that documents setting it has already reported failure. Nothing clears it on success, so a nonzeroerrnoafter a call that worked means nothing. Seterrno = 0before any call whose failure is signalled only through it,strtol'sERANGEbeing this lesson's case. Render the message, not the number:strerror(errno)from<string.h>gives the text, andperror("context")printscontext: messagestraight to standard error.strtol(text, &end, 10)reports how far it got.end == textis no digits,*end != '\0'is trailing junk, anderrno == ERANGEis overflow withLONG_MAXorLONG_MINreturned.atoicannot tell"0"from"oops", which is why this course does not use it.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Handling Failure - Quiz
Test your understanding of the lesson.
Practice Exercises
A Parser That Refuses Bad Input
Write the strict integer parser this lesson built, and report its failures where failures belong. main is given to you and does the reading with the chapter 5 loop, fgets into a 128 byte line, the newline stripped with the strlen check, one call to parse_long per line, and a final summary line in the form "parsed M of N lines" that prints on every run including the empty one. What is missing is parse_long(const char *text, long *out), which returns 1 when text is a whole valid number and 0 otherwise, and writes the value through out only when it returns 1. Use the full strtol discipline. Set errno = 0 before the call, because ERANGE is the only channel strtol has for reporting an overflow and a stale value left there by some earlier call would otherwise look like this call's failure. Pass the address of a char *end so strtol can tell you where it stopped, then judge the result: end == text means it converted nothing at all, *end != '\0' means it converted a prefix and stopped at trailing junk, and errno == ERANGE means the digits were fine but the value does not fit in a long. Each of those three is a rejection. The part to get right is where the complaints go. Every rejection message must be written to STANDARD ERROR with fprintf(stderr, ...) or perror(text), never with printf, because this platform compares your standard output against the expected output and a diagnostic printed there would corrupt a run that is otherwise correct. Nothing about the wording of your error messages is checked, so write messages you would actually want to read; only the "parsed N" lines and the summary are compared. Two behaviours of strtol are worth knowing before you start, because they show up in the test cases: it skips leading whitespace, so " 12" is a valid 12, and it accepts a leading sign, so "+5" is 5 and "-17" is -17. An empty line, which is what a blank line becomes after the newline is stripped, has no digits at all and is a rejection like any other. main returns 0 on every path, since the checker treats a nonzero exit status as a failure however right the output looks.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!