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
- Write a function that opens a file, reads one line and closes it, with a status code for each failure and one cleanup path.
- Use setjmp to recover from a division by zero in a nested call, and print the value passed to longjmp.
- 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.
-
When is the value of
errnomeaningful?Answer
Only after a call that failed and set it
A successful call may leave the old number in place, so the caller resets
errnobefore the call and checks the result first. -
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.
-
What does a single
donelabel 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.
-
What does
longjmp(place, 7)makesetjmpreturn?Answer
The value 7 on its second return
setjmpreturns zero when it records the place and the value passed tolongjmpwhen control comes back. -
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
ENOENTis 2 here, andstrerrorprints the system text for it. -
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
OK0,EMPTY1 andTOO_LARGE2, and the loop prints each status. -
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.
-
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
setjmpwith 7, so the handler branch prints the recovery line. -
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
ERANGEvalue, so 34 remains. -
What does GCC report for a call that ignores
warn_unused_result?Answer
A warning that the return value is ignored
-Wunused-resultnames 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