Error Handling and Input Validation
Return codes, errno, NULL checks, and never trusting input.
7 minute read
Big idea
C has no exceptions. A function signals failure through its return value — NULL, -1, EOF, or a count that's too small — and it's your job to check it, every time.
Return codes
The standard library follows a few patterns. You've already met most of them:
| Function | Failure looks like |
|---|---|
malloc, calloc, realloc, fopen | returns NULL |
scanf, fscanf | returns fewer items than asked for (or EOF) |
fgets | returns NULL |
fclose, fputs | returns EOF |
main | return 0 for success, non-zero for failure |
Your own functions can do the same. A common style: return 0 for success and a non-zero code for "what went wrong", passing the real result back through a pointer:
#include <stdio.h>
enum { OK = 0, ERR_DIV_ZERO = 1 };
int safe_divide(int a, int b, int *result) {
if (b == 0) {
return ERR_DIV_ZERO;
}
*result = a / b;
return OK;
}
int main(void) {
int pairs[][2] = {{10, 2}, {7, 0}, {9, 3}};
for (int i = 0; i < 3; i++) {
int q;
if (safe_divide(pairs[i][0], pairs[i][1], &q) == OK) {
printf("%d / %d = %d\n", pairs[i][0], pairs[i][1], q);
} else {
printf("%d / %d: cannot divide by zero\n", pairs[i][0], pairs[i][1]);
}
}
return 0;
}output
10 / 2 = 5
7 / 0: cannot divide by zero
9 / 3 = 3Run and edit it in the app.
Watch it
errno and perror: why did it fail?
NULL says that fopen failed, not why. For that, many library functions set a global integer called errno (from <errno.h>). Two helpers turn it into words:
perror("prefix")— printsprefix: reasonto stderr, the error stream.strerror(errno)(from<string.h>) — returns the reason as a string.
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void) {
FILE *f = fopen("no-such-file.txt", "r");
if (f == NULL) {
printf("open failed: %s\n", strerror(errno));
perror("fopen"); // the same, on stderr
} else {
fclose(f);
}
return 0;
}Run and edit it in the app.
Read errno straight after the call that failed — the next library call may overwrite it. And it's only meaningful when the return value already said "failed".
Check every allocation
int *a = malloc(n * sizeof *a);
if (a == NULL) {
fprintf(stderr, "out of memory\n");
return 1;
}
fprintf(stderr, …) writes to the error stream, so error messages don't mix with the program's real output.
Input validation
Users type anything. scanf("%d", &n) on the input abc returns 0 and leaves n unchanged — so always check its return value:
#include <stdio.h>
int main(void) {
int age = 0;
scanf("%d", &age);
printf("You are %d\n", age);
return 0;
}input
abcoutput
Not a numberRun and edit it in the app.
The fix: if (scanf("%d", &age) != 1) { printf("Not a number\n"); return 1; }.
scanf still lets some junk through: 12abc reads 12 happily. For strict checking, read a whole word or line as text, then convert it with strtol, which tells you exactly where it stopped:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
/* Returns 1 and sets *out if s is a whole number in [lo, hi]; else 0. */
int parse_int(const char *s, long lo, long hi, long *out) {
char *end;
errno = 0;
long v = strtol(s, &end, 10);
if (end == s) return 0; // no digits at all
if (*end != '\0') return 0; // junk after the number
if (errno == ERANGE) return 0; // too big for a long
if (v < lo || v > hi) return 0; // outside the allowed range
*out = v;
return 1;
}
int main(void) {
char word[64];
while (scanf("%63s", word) == 1) {
long age;
if (parse_int(word, 0, 150, &age)) {
printf("ok: %ld\n", age);
} else {
printf("invalid: %s\n", word);
}
}
return 0;
}input
42 abc 12abc -5 150 999output
ok: 42
invalid: abc
invalid: 12abc
invalid: -5
ok: 150
invalid: 999Run and edit it in the app.
strtol(s, &end, 10) converts base-10 digits and sets end to the first character it didn't use. If end is still s, there were no digits; if *end isn't '\0', there was junk after them.
The user types hello for scanf("%d", &n). What does scanf return?
Common mistake
atoi("abc") returns 0 — exactly the same as atoi("0"). It can't report errors, so it's fine for quick tests but not for user input. Use strtol.
Think of it like
A delivery driver who doesn't knock twice. If the parcel couldn't be delivered, they leave a slip (the return code). Nobody chases you — if you never look at the slip, you'll never know your parcel is back at the depot.
Key terms
- Return code — A value that says whether a function succeeded.
errno— A global set by library functions to say why they failed.perror/strerror— Turnerrnointo a readable message.- stderr — The output stream for error messages.
- Input validation — Checking input is the right shape and range before using it.
strtol— Converts text to along, reporting where it stopped.
Check yourself
How does fopen tell you it failed?
Where should error messages go?
Why prefer strtol to atoi for user input?