Error handling

C reports failure in three ways: a return value, a global error number, or a jump out of a deep call chain. The rule common to all three is that the caller must check, and the language will not remind it. This lesson shows each mechanism with the code that actually handles the failure.

errno and strerror

Many library functions set errno when they fail and return a sentinel such as NULL or -1. The number is only meaningful after a failure and only until the next library call that may set it. strerror turns the number into the message the system would print.

#include <errno.h> #include <stdio.h> #include <string.h> int main(void) { FILE *file = fopen("no-such-file.txt", "r"); if (file == NULL) { printf("errno: %d\n", errno); printf("message: %s\n", strerror(errno)); return 1; } fclose(file); return 0; }

gcc -Wall -Wextra errno.c -o errno
./errno; echo "exit: $?"

errno: 2
message: No such file or directory
exit: 1

2 is ENOENT on this system; the numeric value is part of the ABI but the macro is the portable name. The file functions return to lesson 55, where the same check appears on every open.

Status codes

A function that can fail in several ways returns an enumeration, and the caller decides what each value means. The status travels separately from the output, so a valid result and a failure cannot be confused. The enum is the one from lesson 38.

#include <stdio.h> enum Status { OK, EMPTY, TOO_LARGE }; static enum Status parse(int value) { if (value < 0) { return EMPTY; } if (value > 100) { return TOO_LARGE; } return OK; } int main(void) { int values[3] = {-5, 50, 150}; for (int i = 0; i < 3; i++) { enum Status status = parse(values[i]); printf("%d: %d\n", values[i], status); } return 0; }

gcc -Wall -Wextra codes.c -o codes
./codes

-5: 1
50: 0
150: 2

The numbers are the enum values: 1 is EMPTY, 0 is OK and 2 is TOO_LARGE. Printing the name instead of the number would need the mapping function from lesson 38, but the caller compares against the enumerators either way.

One cleanup path

A function that acquires several resources and can fail between them needs a release for every partial state. The goto done label from lesson 15 keeps the releases in one place, and the same pattern appeared with the leak lesson 32. A huge size would make the first allocation fail and take the same path.

#include <stdio.h> #include <stdlib.h> static int build(int size) { int *first = NULL; int *second = NULL; int status = 0; first = malloc((size_t) size * sizeof *first); if (first == NULL) { status = 1; goto done; } second = malloc((size_t) size * sizeof *second); if (second == NULL) { status = 2; goto done; } for (int i = 0; i < size; i++) { first[i] = i; second[i] = i * i; } printf("last: %d %d\n", first[size - 1], second[size - 1]); done: free(second); free(first); return status; } int main(void) { printf("status: %d\n", build(4)); return 0; }

gcc -Wall -Wextra cleanup.c -o cleanup
./cleanup

last: 3 9
status: 0

The program passes 4, so both allocations succeed and the normal path runs; replacing the size with a value near the top of size_t takes the failure path instead. The important part is that done frees both pointers, and free(NULL) makes the untouched one harmless.

setjmp and longjmp

Deeply nested code sometimes cannot return an error through every level without cluttering each one. setjmp records a place to come back to, and longjmp jumps there with a value. The value is what the second return of setjmp reports, which is how the handler knows that a jump happened.

#include <setjmp.h> #include <stdio.h> static jmp_buf place; static void fail(void) { longjmp(place, 7); } int main(void) { int code = setjmp(place); if (code == 0) { printf("calling fail\n"); fail(); printf("never printed\n"); } else { printf("recovered with %d\n", code); } return 0; }

gcc -Wall -Wextra jump.c -o jump
./jump

calling fail
recovered with 7

The line after fail() never runs, because the jump returns into setjmp with the value 7. This is a sledgehammer: local variables changed between the two points may be indeterminate, and no cleanup runs automatically, so it is used for a top-level recovery rather than for ordinary errors.

Stale errno, ignored results

errno is not cleared by a successful call, so a value read without setting it to zero first may belong to an earlier failure. The program shows a conversion that overflows, followed by one that succeeds, with the old number still in place:

#include <errno.h> #include <stdio.h> #include <stdlib.h> int main(void) { char *end = NULL; errno = 0; (void) strtol("999999999999999999999", &end, 10); printf("first errno: %d\n", errno); long value = strtol("42", &end, 10); printf("value: %ld, errno still: %d\n", value, errno); return 0; }

gcc -Wall -Wextra stale.c -o stale
./stale

first errno: 34
value: 42, errno still: 34

34 is ERANGE, left from the first conversion. The second call succeeded and did not touch it, so a caller that checked only errno would reject a valid number. Set it to zero before the call and check the return value first.

The second mistake is ignoring a result that the function marks as important. The warn_unused_result attribute makes the compiler report a call whose value is discarded:

#include <stdio.h> static int save(int value) __attribute__((warn_unused_result)); static int save(int value) { return value * 2; } int main(void) { save(21); return 0; }

gcc -Wall -Wextra ignored.c -o ignored

ignored.c: In function 'main':
ignored.c:12:5: warning: ignoring return value of 'save' declared with attribute 'warn_unused_result' [-Wunused-result]
12 | save(21);
| ^~~~~~~~

The attribute is the API author's way of saying that the result is the point of the call. It is the same idea as the return codes in this lesson: a function that reports failure must be asked for its answer.

Exercises

  1. Write a function that opens a file, reads one line and closes it, with a status code for each failure and one cleanup path.
  2. Use setjmp to recover from a division by zero in a nested call, and print the value passed to longjmp.
  3. Add warn_unused_result to a function of your own and check that both the warning and a correct use compile.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. When is the value of errno meaningful?

    Answer

    Only after a call that failed and set it

    A successful call may leave the old number in place, so the caller resets errno before the call and checks the result first.

  2. Why does a status enum travel separately from the output?

    Answer

    So a valid result cannot be mistaken for failure

    The status says what happened and the output carries the data, so a good value and a failure are different things.

  3. What does a single done label give a function with several resources?

    Answer

    One release path that every failure reaches

    Each failure jumps to the same label, so every acquired resource is released in one place.

  4. What does longjmp(place, 7) make setjmp return?

    Answer

    The value 7 on its second return

    setjmp returns zero when it records the place and the value passed to longjmp when control comes back.

  5. What does this program print for the missing file? #include <errno.h> #include <stdio.h> #include <string.h> int main(void){ FILE *f=fopen("no-such-file.txt","r"); if(f==NULL){ printf("errno: %d\n", errno); printf("message: %s\n", strerror(errno)); return 1; } fclose(f); return 0; }

    Answer

    errno: 2 / message: No such file or directory

    ENOENT is 2 here, and strerror prints the system text for it.

  6. What does this program print? #include <stdio.h> enum Status { OK, EMPTY, TOO_LARGE }; static enum Status parse(int v){ if(v<0) return EMPTY; if(v>100) return TOO_LARGE; return OK; } int main(void){ int v[3]={-5,50,150}; for(int i=0;i<3;i++) printf("%d: %d\n",v[i],parse(v[i])); return 0; }

    Answer

    -5: 1 / 50: 0 / 150: 2

    The enum values are OK 0, EMPTY 1 and TOO_LARGE 2, and the loop prints each status.

  7. What does this program print for build(4)? #include <stdio.h> #include <stdlib.h> static int build(int size){ int *first=NULL,*second=NULL,status=0; first=malloc((size_t)size*sizeof *first); if(first==NULL){status=1;goto done;} second=malloc((size_t)size*sizeof *second); if(second==NULL){status=2;goto done;} for(int i=0;i<size;i++){first[i]=i;second[i]=i*i;} printf("last: %d %d\n", first[size-1], second[size-1]); done: free(second); free(first); return status; } int main(void){ printf("status: %d\n", build(4)); return 0; }

    Answer

    last: 3 9 / status: 0

    Both allocations succeed, the loop fills four elements, and the last pair is 3 and 9.

  8. What does this program print? #include <setjmp.h> #include <stdio.h> static jmp_buf place; static void fail(void){ longjmp(place,7); } int main(void){ int code=setjmp(place); if(code==0){ printf("calling fail\n"); fail(); printf("never printed\n"); } else printf("recovered with %d\n",code); return 0; }

    Answer

    calling fail / recovered with 7

    The jump returns to setjmp with 7, so the handler branch prints the recovery line.

  9. What does this program print? #include <errno.h> #include <stdio.h> #include <stdlib.h> int main(void){ char *end=NULL; errno=0; (void)strtol("999999999999999999999",&end,10); printf("first errno: %d\n",errno); long v=strtol("42",&end,10); printf("value: %ld, errno still: %d\n",v,errno); return 0; }

    Answer

    first errno: 34 / value: 42, errno still: 34

    The successful call does not clear the old ERANGE value, so 34 remains.

  10. What does GCC report for a call that ignores warn_unused_result?

    Answer

    A warning that the return value is ignored

    -Wunused-result names the function and the line where its result was discarded.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev