C Part I · Module 12 · Standard Library, Files and Program Interfaces
errno and Library Error Reporting
A message saying only that a file could not be opened leaves its reader to guess between a missing file, a spelling mistake, a permission problem and a path that turned out to be a directory — four situations with four different fixes and one indistinguishable message. The library knew which one it was, and it recorded the answer in a place this module has touched twice without examining. The contract on that place has more conditions attached than any other in the standard library: the value is meaningful only at one particular moment, nothing ever resets it, a successful call is free to change it, and — as this chapter measures — the two examples every textbook gives for it do not actually use it on the machine these pages were written on.
Intermediate19 min readCPart IFundamentalsStandard Library
Part I · Module 12 · Chapter 12.8 · errno and Library Error Reporting
1. What errno Is
#include <errno.h>errno is a modifiable lvalue of type int that library functions set to indicate why something failed. It is not necessarily a variable — the standard allows it to be a macro expanding to a function call or a thread-local object, and on this platform taking its address works and yields a per-thread location.
That detail has one practical consequence worth knowing now: errno is per-thread, so a value set by one thread is not seen by another. You cannot declare extern int errno; yourself; include the header.
ISO C requires exactly three values:
ISO C requires EDOM=33 ERANGE=34 EILSEQ=92| Macro | Meaning |
|---|---|
EDOM | a domain error — the argument is outside what the function accepts |
ERANGE | a range error — the result cannot be represented |
EILSEQ | an invalid multibyte sequence |
Every other name you have seen — ENOENT, EACCES, ENOMEM, EEXIST — is POSIX, not C. They are universally available on Unix-like systems and on Windows, and they are outside the standard this curriculum is written against. The numbers above are this platform's; even the three required macros have implementation-defined values.
2. The Two Facts That Make It Dangerous
A successful call does not clear it. Measured in Chapter 12.1 §4 and reproduced here:
after failed fopen : errno=2 (No such file or directory)
after SUCCESSFUL fopen: errno=2 (No such file or directory)And a successful call is allowed to set it. The standard permits any library function to store a non-zero value in errno even when it succeeds — typically because it tried something internally, that attempt failed, and it recovered. Measured here, a successful sqrt and a successful fread both left it at 0, but that is permission not exercised rather than a guarantee.
Put those together and
errno != 0can never be a failure test — not even if you cleared it first. A non-zeroerrnoafter a call may mean the call failed, or that it succeeded after an internal retry, or that an earlier call left a value behind.
So the question errno answers is strictly narrower than it looks:
errnosays why. It never says whether. The function's own return value says whether, by whichever of Chapter 12.1 §3's conventions it uses, and only after that says "failed" doeserrnomean anything.
3. The Only Correct Sequence
static int to_long(const char *s, long *out)
{
errno = 0; /* 1 */
char *end;
long v = strtol(s, &end, 10); /* 2 */
if (end == s || *end != '\0') { return 0; } /* 3 — strtol's own indicators */
if (errno == ERANGE) { return 0; } /* 4 — now errno is meaningful */
*out = v;
return 1;
} 42 -> ok 42
-7 -> ok -7
99999999999999999999 -> rejected
abc -> rejected
5x -> rejected
(empty) -> rejectedSix inputs, six correct answers. Note that two of the three rejections never consult errno at all — abc, 5x and the empty string are detected by end, which is strtol's own indicator. errno is only reached for the overflow, which is the one failure that has no other sign.
Three mistakes the sequence prevents:
/* WRONG: errno may be left over from any earlier call */
FILE *f = fopen(path, "r");
if (errno != 0) { report("could not open"); }
/* WRONG: nothing cleared errno, so a stale ERANGE is indistinguishable */
long v = strtol(s, &end, 10);
if (errno == ERANGE) { reject(); }
/* WRONG: the printf may itself set errno before you read it */
if (f == NULL) {
printf("open failed\n");
fprintf(stderr, "reason: %s\n", strerror(errno));
}The third is the subtlest and the most common. Capture errno before doing anything else, because every intervening library call — including the one printing your message — may change it:
if (f == NULL) {
int saved = errno; /* capture immediately */
fprintf(stderr, "%s: cannot open %s: %s\n", program, path, strerror(saved));
return EXIT_FAILURE;
}4. Which Functions Actually Document It
This is where c.ts's phrase "a function that documents it" does real work, and where the chapter's main finding lives.
strtol documents ERANGE unconditionally. Measured:
strtol overflow = 9223372036854775807, errno == ERANGE ? 1The math functions do not — necessarily. The two canonical textbook examples:
sqrt(-1.0) = nan errno == EDOM ? 0
exp(100000.0) = inf errno == ERANGE ? 0Neither set errno. That is not a bug, and the standard provides the way to ask why:
math_errhandling = 2
MATH_ERRNO bit set ? 0
MATH_ERREXCEPT bit set ? 1
sqrt(-1.0)=nan errno=0 FE_INVALID raised ? 1
exp(100000)=inf errno=0 FE_OVERFLOW raised ? 1The general rule this produces:
Use
errnoonly where the function's contract says it is set, and check whether that promise is conditional. Forstrtol,strtodand the conversion family it is unconditional. For the math functions it depends onmath_errhandling. Forfopen,freadand the rest of<stdio.h>, ISO C says nothing at all — theirerrnobehaviour is POSIX, which is why it works everywhere you are likely to be and is not something the C standard guarantees.
5. Turning a Number into a Message
char *strerror(int errnum); /* <string.h> */
void perror(const char *prefix); /* <stdio.h> */perror : cannot open /no/such/path: No such file or directory
strerror : No such file or directory
strerror(0) = Undefined error: 0
strerror(1) = Operation not permitted
strerror(2) = No such file or directory
strerror(3) = No such process
strerror(9999) = Unknown error: 9999perror writes your prefix, then ": ", then the message for the current errno, then a newline — to stderr. One call, correct destination, and it reads errno itself, so there is no window in which something else could change it.
strerror returns the message as a string, which you can put anywhere. Read with Chapter 12.1's questions it has a notable row: it is convention D — it cannot fail. strerror(9999) returned "Unknown error: 9999" rather than a null pointer, which means you never need to check it and must never treat its result as a failure indication.
Two cautions on strerror:
The returned string may be overwritten by a later call, so copy it if you need it to outlive the next one.
It is not required to be thread-safe. strerror_r (POSIX) and strerror_s (C11 Annex K, optional) exist for that; neither is part of the C17 base.
And the message quality point, which decides whether any of this was worth it:
/* says what you tried */
fprintf(stderr, "%s: cannot open %s\n", program, path);
/* says what you tried AND why it did not work */
fprintf(stderr, "%s: cannot open %s: %s\n", program, path, strerror(saved));The second line turns "something went wrong with this file" into "the file is not there", "you do not have permission", or "that is a directory" — which are the four possibilities a user was otherwise left to guess between.
6. The Complete Shape
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
static int read_config(const char *program, const char *path)
{
errno = 0; /* 1 */
FILE *f = fopen(path, "r"); /* 2 */
if (f == NULL) { /* 3 — fopen's own indicator */
int saved = errno; /* 4 — capture immediately */
fprintf(stderr, "%s: cannot open %s: %s\n",
program, path, strerror(saved));
return -1;
}
char line[256];
while (fgets(line, sizeof line, f) != NULL) {
/* ... */
}
if (ferror(f)) { /* 12.4 §3: which failure */
int saved = errno;
fprintf(stderr, "%s: error reading %s: %s\n",
program, path, strerror(saved));
fclose(f);
return -1;
}
if (fclose(f) != 0) { /* 12.4 §4: the close is the write */
int saved = errno;
fprintf(stderr, "%s: error closing %s: %s\n",
program, path, strerror(saved));
return -1;
}
return 0;
}Five properties, each from a measurement in this module:
errno is cleared before the call, so a stale value cannot be reported.
The function's own indicator decides — f == NULL, ferror, fclose != 0 — and errno is only consulted afterwards.
errno is captured immediately into saved, before the fprintf that might change it.
Every message names the program, the thing, and the reason, which is what distinguishes a diagnostic from a complaint.
Every failure path returns a value the caller can act on, and the caller turns that into Chapter 12.7's exit status.
7. What You Should Be Able to Say Now
errnois a modifiableintlvalue from<errno.h>; it may be a macro, it is per-thread, and you must not declare it yourself.- ISO C requires only
EDOM,ERANGEandEILSEQ— measured as 33, 34 and 92 here.ENOENTand friends are POSIX. - Measured: a successful
fopenlefterrnoat 2. Nothing resets it. - A successful call is also permitted to set it, so
errno != 0is never a failure test. errnosays why, never whether. The function's own return value says whether.- The sequence: clear · call · test the documented indicator · only then read
errno. - Capture
errnoimmediately — thefprintfprinting your message may change it. - Measured:
strtolsetERANGEon overflow — its contract is unconditional. - Measured:
sqrt(-1.0)andexp(100000)did not seterrno.math_errhandlingwas 2 —MATH_ERREXCEPTonly — andFE_INVALID/FE_OVERFLOWwere raised instead. - So the most widely repeated
errnoexample is conditional, andmath_errhandlingis how you ask. For numeric code, test the value. <stdio.h>'serrnobehaviour is POSIX, not ISO C.perrorwrites prefix, reason and newline tostderr;strerrorcannot fail —strerror(9999)returned a message.
8. Exercises
Exercise 1 — fix the sequence. Each has one defect from §3. Name it and correct it.
FILE *f = fopen(p, "r"); if (errno) { fail(); }
long v = strtol(s, &e, 10); if (errno == ERANGE) { reject(); }
if (!f) { printf("failed\n"); perror("open"); }
errno = 0; if (strlen(s) > 10) { if (errno) { fail(); } }Exercise 2 — which values are C's. Sort these into ISO C and POSIX: EDOM, ENOENT, ERANGE, EACCES, EILSEQ, ENOMEM. Say what follows for a program that must compile on a freestanding implementation.
Exercise 3 — explain the math result. In four sentences, explain why sqrt(-1.0) did not set EDOM here, what did happen instead, and how a program can discover which behaviour it will get.
Exercise 4 — capture it. Rewrite this so the reported reason is the one that actually applies.
if (fclose(f) != 0) {
fprintf(stderr, "closing failed\n");
fprintf(stderr, "reason: %s\n", strerror(errno));
}Exercise 5 — which indicator. For each, name the function's own failure indicator and say whether errno is then worth consulting.
fopen fread strtol strlen fseek mallocExercise 6 — write the message. A program fails to open /etc/app.conf. Write the complete fprintf that a user could act on, and list the four different causes your message would now distinguish.
Exercise 7 — the no-failure function. strerror(9999) returned a string. Using Chapter 12.1's conventions, say which one strerror uses, what that means for checking its result, and what you must still be careful about.
Exercise 8 — audit a routine. Given a function that opens a file, reads it, closes it and reports errors, list every place errno should be cleared, every place it should be captured, and every place it should not be consulted at all.
9. Summary
errnois a per-thread modifiableint; ISO C requires onlyEDOM,ERANGE,EILSEQ— the rest are POSIX.- Measured: a successful call left it set, and a successful call may set it.
errno != 0is not a failure test. - It says why, never whether.
- Clear · call · test the indicator · only then read
errno— and capture it immediately. - Measured:
strtolsetsERANGEunconditionally;sqrtandexpset nothing, becausemath_errhandlingwas 2 and the errors were reported as floating-point exceptions. - The most repeated
errnoexample is conditional. For numeric code, test the value. <stdio.h>'serrnobehaviour is POSIX, not ISO C.perroris one call tostderr;strerrorcannot fail and may be overwritten.- A useful message names the program, the thing, and the reason.
Eight chapters have now read contracts, and the reading has mostly been a way of avoiding trouble — what a function assumes, how it reports failure, what it does not promise. There is a cheerier use for the same skill. The library contains a small number of functions that do things you would otherwise write yourself, correctly, in code that has been examined by more people than will ever read yours. Searching an array, sorting one, copying a block of bytes, comparing two, converting text to a number: all of these exist, all of them are faster than the obvious loop, and each comes with preconditions that make the difference between replacing your loop and introducing a bug. The final chapter of this module is that handful — not a catalogue, but the ones worth knowing by name, read the way this module has taught you to read them.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
