Skip to content
FollowtheDamnCode

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:

FunctionFailure looks like
malloc, calloc, realloc, fopenreturns NULL
scanf, fscanfreturns fewer items than asked for (or EOF)
fgetsreturns NULL
fclose, fputsreturns EOF
mainreturn 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:

Return a status, hand back the result 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 = 3

Run 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") — prints prefix: reason to stderr, the error stream.
  • strerror(errno) (from <string.h>) — returns the reason as a string.
Opening a file that isn't there
#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:

Fix the bug: "abc" should be rejected
#include <stdio.h>

int main(void) {
    int age = 0;
    scanf("%d", &age);
    printf("You are %d\n", age);
    return 0;
}
input
abc
output
Not a number

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

Strict number checking with strtol
#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 999
output
ok: 42
invalid: abc
invalid: 12abc
invalid: -5
ok: 150
invalid: 999

Run 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 — Turn errno into 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 a long, 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?