Errors#

C has no exception keyword. The standard error model is return codes: a function returns a sentinel value (typically -1 or NULL) and sets errno to a diagnostic code. The operator checks both, branches on failure, and either propagates the code upward or maps it to something more specific.

setjmp / longjmp provides a non-local-jump back-door for the rare cases where unwinding by hand is impractical; the operator uses it sparingly.

Return codes#

The standard library convention.

Function signature

Failure indicator

int func(...)

returns -1 (or another negative number) on failure, sets errno.

T *func(...)

returns NULL on failure, sets errno.

size_t func(...)

returns (size_t)-1 or a documented sentinel.

FILE *fopen(...)

NULL on failure.

int fd = open(path, O_RDONLY);
if (fd < 0) {
    perror("open");                  /* prints "open: No such file ..." */
    return -1;
}

errno#

errno is a thread-local int declared in <errno.h>. Set by the kernel and the C library on failure; the operator reads it immediately after the failing call (any other libc call may overwrite it).

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

FILE *f = fopen(path, "r");
if (!f) {
    fprintf(stderr, "fopen(%s): %s\n", path, strerror(errno));
    return -1;
}

perror(msg) is equivalent to fprintf(stderr, "%s: %s\n", msg, strerror(errno)).

Common errno values.

Code

Meaning

ENOENT

no such file

EACCES

permission denied

ENOMEM

out of memory

EINVAL

invalid argument

EAGAIN / EWOULDBLOCK

resource temporarily unavailable (non-blocking I/O)

EINTR

interrupted system call

EPIPE

broken pipe

ETIMEDOUT

operation timed out

Custom error codes#

For internal APIs, the operator defines an enum of error codes plus a const char *_str() helper.

typedef enum {
    SCAN_OK = 0,
    SCAN_ERR_DNS,
    SCAN_ERR_CONNECT,
    SCAN_ERR_TIMEOUT,
    SCAN_ERR_INTERNAL,
} scan_err_t;

const char *scan_strerror(scan_err_t e) {
    switch (e) {
    case SCAN_OK:           return "ok";
    case SCAN_ERR_DNS:      return "DNS lookup failed";
    case SCAN_ERR_CONNECT:  return "connect failed";
    case SCAN_ERR_TIMEOUT:  return "timed out";
    default:                return "unknown";
    }
}

goto cleanup#

The standard pattern for propagating errors past resource acquisition. Each acquisition is paired with a goto that jumps to a single cleanup block.

int run(const char *path) {
    int   rc  = -1;
    FILE *f   = NULL;
    char *buf = NULL;

    f = fopen(path, "r");
    if (!f) goto err;

    buf = malloc(SIZE);
    if (!buf) goto err;

    if (process(f, buf) < 0) goto err;

    rc = 0;
err:
    free(buf);
    if (f) fclose(f);
    return rc;
}

assert#

<assert.h>’s assert(cond) aborts the program with a diagnostic when cond is false. Used for programmer errors you want to surface at the moment of failure (violated invariants, NULL arguments to “this never receives NULL” functions).

#include <assert.h>

void send(buf_t *b) {
    assert(b != NULL);
    /* … */
}

Compiled away by -DNDEBUG. Do not use assert for input validation; assertions disappear in release builds.

For checks you want to keep in release, static_assert (_Static_assert in C11; static_assert keyword in C23) runs at compile time.

static_assert(sizeof(uint32_t) == 4, "uint32_t must be 4 bytes");

abort and exit#

abort() raises SIGABRT and terminates without running atexit handlers. exit(status) runs cleanup and exits with the given code. _Exit(status) exits immediately, skipping cleanup, the way a child process should after fork.

if (impossible) abort();
exit(EXIT_SUCCESS);

setjmp / longjmp#

The C non-local jump. setjmp saves the calling environment into a jmp_buf; longjmp jumps back, restoring the environment.

#include <setjmp.h>

static jmp_buf handler;

void run(void) {
    if (setjmp(handler) == 0) {
        /* normal path */
        work();
    } else {
        /* arrived here via longjmp */
        cleanup();
    }
}

void on_fatal(void) {
    longjmp(handler, 1);
}

Avoid longjmp outside library code; it bypasses goto cleanup blocks and leaks resources unless the operator unwinds by hand. The kernel never uses it.

Signals#

A signal handler is the closest thing C has to an asynchronous exception. <signal.h>’s signal(SIG, handler) installs one; sigaction is the saner POSIX form.

#include <signal.h>

volatile sig_atomic_t stop = 0;

static void handle_sigint(int sig) { stop = 1; }

int main(void) {
    struct sigaction sa = {.sa_handler = handle_sigint};
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);

    while (!stop) work();
    return 0;
}

Only async-signal-safe functions are legal inside a handler (write, _exit, signal); printf and malloc are not.

References#