Skip to content
Laviyx Learn

Try

C course menu

Error Handling in C: Return Codes, errno, perror and assert

Handle errors in C, which has no exceptions: checking return values, error codes and out-parameters, errno with perror and strerror, cleaning up with goto, reporting errors to stderr, exit and EXIT_FAILURE, and assert for programming mistakes.

Advanced11 min readUpdated
On this page

C has no exceptions. Functions report failure through their return value, and the caller must check it. Ignoring return values is one of the most common sources of bugs in C programs.

Checking return values#

Standard library functions signal failure in documented ways:

FunctionOn failure returns
fopen, malloc, fgets, strchrNULL
scanf, fscanfFewer items than requested, or EOF
fclose, fputs, printfEOF or a negative value
fread, fwriteFewer items than requested

Look up each function's failure value, and check it.

Designing your own functions#

A common style returns a status code and passes results back through pointers:

C
#include <stdio.h>

typedef enum { OK = 0, ERR_DIVIDE_BY_ZERO, ERR_OUT_OF_RANGE } Status;

const char *status_text(Status s) {
    switch (s) {
        case OK:                 return "ok";
        case ERR_DIVIDE_BY_ZERO: return "division by zero";
        case ERR_OUT_OF_RANGE:   return "result out of range";
    }
    return "unknown error";
}

Status percentage(int part, int whole, int *out) {
    if (whole == 0) return ERR_DIVIDE_BY_ZERO;
    int p = part * 100 / whole;
    if (p < 0 || p > 100) return ERR_OUT_OF_RANGE;
    *out = p;
    return OK;
}

int main(void) {
    int tests[][2] = {{45, 60}, {5, 0}, {80, 40}};
    for (int i = 0; i < 3; i++) {
        int result;
        Status s = percentage(tests[i][0], tests[i][1], &result);
        if (s == OK) printf("%d/%d = %d%%\n", tests[i][0], tests[i][1], result);
        else printf("%d/%d: error: %s\n", tests[i][0], tests[i][1], status_text(s));
    }
    return 0;
}
Output
45/60 = 75%
5/0: error: division by zero
80/40: error: result out of range

Other conventions: return -1 for failure and 0 or a count for success (common in POSIX functions), or return NULL for a failed pointer result. Whatever you choose, use it consistently and document it.

errno, perror and strerror#

Many library functions also set the global variable errno (from <errno.h>) to a code describing why they failed. perror prints your message followed by the description; strerror(errno) returns the description as a string:

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

int main(void) {
    FILE *f = fopen("no-such-file.txt", "r");
    if (f == NULL) {
        int err = errno;              /* save it: later calls may change errno */
        printf("errno %d (%s)\n", err, err == ENOENT ? "ENOENT" : "other");
        printf("strerror: %s\n", strerror(err));
        errno = err;
        perror("no-such-file.txt");   /* writes to stderr */
        return 0;
    }
    fclose(f);
    return 0;
}
Output
errno 2 (ENOENT)
strerror: No such file or directory
no-such-file.txt: No such file or directory
  • Check errno only after a function has reported failure; successful calls may leave any value in it.
  • Save errno in a variable straight away if you will call other functions before using it.
  • strtol sets errno to ERANGE when a number is too large, so set errno = 0 before calling it and check afterwards.
  • The message text can differ between systems; the constants such as ENOENT are portable.

Cleaning up on errors#

When a function acquires several resources, each failure must release what was already acquired. A goto to cleanup labels in reverse order keeps this tidy:

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

int copy_upper(const char *src_path, const char *dst_path) {
    int result = -1;
    FILE *in = NULL, *out = NULL;
    char *buf = NULL;

    in = fopen(src_path, "r");
    if (in == NULL) goto done;
    out = fopen(dst_path, "w");
    if (out == NULL) goto done;
    buf = malloc(256);
    if (buf == NULL) goto done;

    while (fgets(buf, 256, in) != NULL) {
        for (char *p = buf; *p; p++) if (*p >= 'a' && *p <= 'z') *p -= 32;
        fputs(buf, out);
    }
    result = 0;

done:
    free(buf);                   /* free(NULL) is safe */
    if (out) fclose(out);
    if (in) fclose(in);
    return result;
}

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

    printf("first: %d\n", copy_upper("in.txt", "out.txt"));
    printf("second: %d\n", copy_upper("missing.txt", "out2.txt"));

    char line[32];
    f = fopen("out.txt", "r");
    if (f == NULL) return 1;
    while (fgets(line, sizeof line, f)) printf("%s", line);
    fclose(f);
    return 0;
}
Output
first: 0
second: -1
HELLO
ERRORS

Whatever fails, every resource acquired so far is released exactly once, and the function returns -1 to its caller.

exit#

exit(status) (from <stdlib.h>) ends the program immediately from anywhere, flushing and closing open FILEs. Use EXIT_SUCCESS and EXIT_FAILURE. It is fine for small tools; library code should return errors to the caller instead and let it decide.

assert: catching programming mistakes#

assert(condition) from <assert.h> stops the program with a message if the condition is false. Use it for things that can only be false because of a bug, not for user input or failures such as a missing file:

C
#include <assert.h>

double average(const int *a, int n) {
    assert(a != NULL && n > 0);      /* callers must never pass these */
    /* ... */
}

When it fails, it prints the expression, the file and the line, then aborts. Compiling with -DNDEBUG removes all asserts, so never put necessary work inside one.

Practice#

Practice exercise

Write int parse_port(const char *text, int *port) that returns 0 on success and -1 on failure. It should reject empty strings, extra characters, and values outside 1 to 65535, and use errno to catch numbers too large for a long. Test it with "8080", "0", "80a" and "99999999999999999999".

Show solution
C
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

int parse_port(const char *text, int *port) {
    char *end;
    errno = 0;
    long v = strtol(text, &end, 10);
    if (end == text || *end != '\0') return -1;    /* empty or extra text */
    if (errno == ERANGE) return -1;                /* too large for long */
    if (v < 1 || v > 65535) return -1;
    *port = (int)v;
    return 0;
}

int main(void) {
    const char *inputs[] = {"8080", "0", "80a", "99999999999999999999"};
    for (int i = 0; i < 4; i++) {
        int port;
        if (parse_port(inputs[i], &port) == 0) printf("%s -> port %d\n", inputs[i], port);
        else printf("%s -> invalid\n", inputs[i]);
    }
    return 0;
}
Output
8080 -> port 8080
0 -> invalid
80a -> invalid
99999999999999999999 -> invalid

Key takeaway#

Finished this lesson?