Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    #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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ISO C requires EDOM=33 ERANGE=34 EILSEQ=92
MacroMeaning
EDOMa domain error — the argument is outside what the function accepts
ERANGEa range error — the result cannot be represented
EILSEQan 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 != 0 can never be a failure test — not even if you cleared it first. A non-zero errno after 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:

errno says 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" does errno mean anything.

3. The Only Correct Sequence

Four steps for using errno correctly. First, set errno to zero immediately before the call, because nothing else ever resets it and a successful earlier call may have left a value behind. Second, make the call. Third, test the function's own documented failure indicator, such as a null return or a short count — this, not errno, is what decides whether anything failed. Fourth, only if that indicator reported failure, read errno to find out why. Reading errno before step three asks a question that has no answer.The only sequence in which errno means anything01SET ERRNO = 0nothing else ever resets it02MAKE THE CALLand keep its return value03TEST THE DOCUMENTED INDICATORNULL, a short count, a sentinel — this decides whether it failed04ONLY THEN READ ERRNOit says why, never whether
Figure 1 — the four steps, in the only order that works. Step one exists because nothing in the library ever resets errno, so a value left over from any earlier call — including one inside the library that you never made — is still sitting there. Step three is the one people skip: the function's own return value is what decides whether anything failed, and errno is consulted only once that decision has already been made. Reading errno before step three is asking a question that has no answer, because the value may be describing a failure that happened somewhere else entirely.
Azvya Education Pvt. Ltd.VLSI Mentor
the pattern, applied
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;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  42                     -> ok  42
  -7                     -> ok  -7
  99999999999999999999   -> rejected
  abc                    -> rejected
  5x                     -> rejected
  (empty)                -> rejected

Six 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    /* 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
strtol overflow = 9223372036854775807, errno == ERANGE ? 1

The math functions do not — necessarily. The two canonical textbook examples:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sqrt(-1.0)    = nan   errno == EDOM   ? 0
exp(100000.0) = inf   errno == ERANGE ? 0

Neither set errno. That is not a bug, and the standard provides the way to ask why:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 ? 1

The general rule this produces:

Use errno only where the function's contract says it is set, and check whether that promise is conditional. For strtol, strtod and the conversion family it is unconditional. For the math functions it depends on math_errhandling. For fopen, fread and the rest of <stdio.h>, ISO C says nothing at all — their errno behaviour 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    char *strerror(int errnum);          /* <string.h> */
    void  perror(const char *prefix);    /* <stdio.h>  */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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: 9999

perror 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    /* 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

Azvya Education Pvt. Ltd.VLSI Mentor
an error report that is actually useful
#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

  1. errno is a modifiable int lvalue from <errno.h>; it may be a macro, it is per-thread, and you must not declare it yourself.
  2. ISO C requires only EDOM, ERANGE and EILSEQ — measured as 33, 34 and 92 here. ENOENT and friends are POSIX.
  3. Measured: a successful fopen left errno at 2. Nothing resets it.
  4. A successful call is also permitted to set it, so errno != 0 is never a failure test.
  5. errno says why, never whether. The function's own return value says whether.
  6. The sequence: clear · call · test the documented indicator · only then read errno.
  7. Capture errno immediately — the fprintf printing your message may change it.
  8. Measured: strtol set ERANGE on overflow — its contract is unconditional.
  9. Measured: sqrt(-1.0) and exp(100000) did not set errno. math_errhandling was 2 — MATH_ERREXCEPT only — and FE_INVALID/FE_OVERFLOW were raised instead.
  10. So the most widely repeated errno example is conditional, and math_errhandling is how you ask. For numeric code, test the value.
  11. <stdio.h>'s errno behaviour is POSIX, not ISO C.
  12. perror writes prefix, reason and newline to stderr; strerror cannot fail — strerror(9999) returned a message.

8. Exercises

Exercise 1 — fix the sequence. Each has one defect from §3. Name it and correct it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fopen    fread    strtol    strlen    fseek    malloc

Exercise 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

  • errno is a per-thread modifiable int; ISO C requires only EDOM, ERANGE, EILSEQ — the rest are POSIX.
  • Measured: a successful call left it set, and a successful call may set it. errno != 0 is not a failure test.
  • It says why, never whether.
  • Clear · call · test the indicator · only then read errno — and capture it immediately.
  • Measured: strtol sets ERANGE unconditionally; sqrt and exp set nothing, because math_errhandling was 2 and the errors were reported as floating-point exceptions.
  • The most repeated errno example is conditional. For numeric code, test the value.
  • <stdio.h>'s errno behaviour is POSIX, not ISO C.
  • perror is one call to stderr; strerror cannot 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.