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 |
|---|---|
|
returns |
|
returns |
|
returns |
|
|
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 |
|---|---|
|
no such file |
|
permission denied |
|
out of memory |
|
invalid argument |
|
resource temporarily unavailable (non-blocking I/O) |
|
interrupted system call |
|
broken pipe |
|
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#
Control flow for
goto cleanupflow.I/O for
perror/strerrorand stdio error reporting.Concurrency for signal-safe code in handlers.