Error Handling

C has no exceptions. Functions report failure through their return values — a special value like NULL or -1, a status code — and often set errno to say why. Checking those results consistently is what separates robust C programs from fragile ones.

Checking return values

Most library functions signal failure in their return value. Know the convention for each one you use:

FunctionOn failure returns
fopen, malloc, calloc, reallocNULL
fclose, fflush, remove, renameNon-zero (EOF for fclose)
scanf, fscanf, sscanfFewer items than expected, or EOF
fgetsNULL (at end of input or on error)
fread, fwriteFewer items than requested
printf, fprintfA negative number
strtol, strtodSets errno to ERANGE; check the end pointer for junk

errno, perror and strerror

Many functions set the global errno (from <errno.h>) to a code describing the failure. perror prints your message plus the system's description; strerror returns the description as a string:

C
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(void) {
    FILE *f = fopen("no-such-folder/settings.ini", "r");
    if (f == NULL) {
        int saved = errno;                 // save it before calling anything else
        perror("could not open settings");
        fprintf(stderr, "error code %d: %s\n", saved, strerror(saved));
        return 1;
    }
    fclose(f);
    return 0;
}
Output
could not open settings: No such file or directory
error code 2: No such file or directory

errno is only meaningful right after a function has reported failure — successful calls may leave any value in it — so check the return value first, then read errno straight away.

Designing functions that can fail

Choose one convention and use it throughout a project. Common choices:

  • Return a pointer, NULL on failure — for functions that create something.
  • Return bool or 0/-1, with results through pointer parameters (lesson 34).
  • Return an error-code enum — when callers need to tell different failures apart.
C
#include <stdio.h>
#include <string.h>

typedef enum {
    PARSE_OK = 0,
    PARSE_EMPTY,
    PARSE_TOO_LONG,
    PARSE_BAD_CHARACTER,
} ParseError;

static const char *const parse_messages[] = {
    [PARSE_OK] = "ok",
    [PARSE_EMPTY] = "username is empty",
    [PARSE_TOO_LONG] = "username is longer than 12 characters",
    [PARSE_BAD_CHARACTER] = "username may only contain letters, digits and _",
};

ParseError check_username(const char *s) {
    size_t len = strlen(s);
    if (len == 0) return PARSE_EMPTY;
    if (len > 12) return PARSE_TOO_LONG;
    for (size_t i = 0; i < len; i++) {
        char c = s[i];
        if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')) {
            return PARSE_BAD_CHARACTER;
        }
    }
    return PARSE_OK;
}

int main(void) {
    const char *names[] = { "sam_r", "", "a_very_long_username", "sam rivera" };
    for (int i = 0; i < 4; i++) {
        ParseError e = check_username(names[i]);
        printf("%-22s %s\n", names[i], parse_messages[e]);
    }
    return 0;
}
Output
sam_r                  ok
                       username is empty
a_very_long_username   username is longer than 12 characters
sam rivera             username may only contain letters, digits and _

Cleaning up on failure

When a function acquires several resources, every failure path must release what was already acquired. The goto cleanup pattern from lesson 20 keeps that manageable:

C
#include <stdio.h>
#include <stdlib.h>

int copy_file(const char *from, const char *to) {
    int result = -1;
    FILE *in = NULL, *out = NULL;
    char *buffer = NULL;

    in = fopen(from, "rb");
    if (in == NULL) { perror(from); goto cleanup; }
    out = fopen(to, "wb");
    if (out == NULL) { perror(to); goto cleanup; }
    buffer = malloc(4096);
    if (buffer == NULL) { perror("malloc"); goto cleanup; }

    size_t n;
    while ((n = fread(buffer, 1, 4096, in)) > 0) {
        if (fwrite(buffer, 1, n, out) != n) { perror("write"); goto cleanup; }
    }
    if (ferror(in)) { perror("read"); goto cleanup; }
    result = 0;

cleanup:
    free(buffer);
    if (out != NULL && fclose(out) != 0) result = -1;
    if (in != NULL) fclose(in);
    return result;
}

int main(void) {
    FILE *f = fopen("original.txt", "w");
    if (f == NULL) return 1;
    fputs("important data\n", f);
    fclose(f);

    printf("copy 1: %s\n", copy_file("original.txt", "backup.txt") == 0 ? "ok" : "failed");
    printf("copy 2: %s\n", copy_file("missing.txt", "backup2.txt") == 0 ? "ok" : "failed");
    return 0;
}
Output
copy 1: ok
missing.txt: No such file or directory
copy 2: failed

exit and atexit

exit(status) ends the program from anywhere: it flushes and closes open files and runs functions registered with atexit. It's fine for small tools; in libraries and larger programs, return errors to the caller instead so it can decide what to do.

C
#include <stdio.h>
#include <stdlib.h>

void goodbye(void) {
    printf("cleaning up before exit\n");
}

void must_be_positive(int n) {
    if (n <= 0) {
        fprintf(stderr, "fatal: expected a positive number, got %d\n", n);
        exit(EXIT_FAILURE);
    }
}

int main(void) {
    atexit(goodbye);
    must_be_positive(5);
    printf("5 is fine\n");
    must_be_positive(-2);
    printf("never printed\n");
    return EXIT_SUCCESS;
}
Output
5 is fine
fatal: expected a positive number, got -2
cleaning up before exit

assert: checking assumptions

assert(condition) from <assert.h> stops the program with a message if the condition is false. Use it for things that should never happen if your code is correct — not for bad user input. Assertions are removed when you compile with -DNDEBUG, so they must not have side effects.

C
#include <assert.h>

double average(const double *values, size_t n) {
    assert(values != NULL && n > 0);   // a caller bug, not a user error
    ...
}

Try it yourself

Write read_config_number, which opens a file, reads one integer, and returns a distinct error code for "file missing", "no number" and success.

TXT limit.txt
250
Try it yourself · main.c
Edit the code, copy it into main.c, then compile and run it with cc -std=c17 -Wall main.c -o main && ./main. How to set up a C compiler
Output
limit.txt: error 0, limit 250
missing.txt: error 1, limit still 250