Error Handling & Robustness

error propagation and an error code

Imagine a chain of people passing a message up a tall ladder. A worker at the bottom finds a problem he cannot fix - a cracked beam - and shouts it up. Each person above relays it upward until it reaches the foreman, who is the one with the authority and the wide view needed to decide what to do. Error propagation is exactly this: when a function hits a failure it cannot or should not handle itself, it passes the failure up to whoever called it, and so on, until it reaches code that can actually make a sensible decision.

The mechanism in C is built on return values. A low-level function that fails returns a value signalling failure - a sentinel like -1 or NULL, or a specific error code. Its caller checks that return value and, if it cannot handle the problem here, returns a failure to its own caller in turn. The failure ripples up the call chain. The error code is how you carry which failure it was, not just that one happened: rather than a bare -1, a function can return a value from an enumeration of named error codes - for example an enum with OK = 0, ERR_NOT_FOUND, ERR_PERMISSION, ERR_OUT_OF_MEMORY - so each layer can either react to a specific kind or pass it along unchanged. POSIX's errno is one shared variant of this idea; a returned enum or an out-parameter is another; richer languages use a Result type that carries either a value or a structured error, and provide syntax (Rust's ? operator) that propagates the error automatically if you do not handle it. The key discipline is that propagating is a deliberate choice, not forgetting: you looked at the failure, decided this layer is not the right place to handle it, and passed it up cleanly - crucially, releasing any resources this layer acquired on the way out, so propagation does not leak.

Why it matters: propagation is what lets the decision about an error be made at the right level. The function that opens a config file knows how to open files but has no idea whether a missing config should abort the program, fall back to defaults, or prompt the user - only higher-level code knows that. Propagation carries the failure to where that knowledge lives. The honest caveats: a long propagation chain can lose context (a bare 'error code 5' twelve layers up tells you little about where it came from), which is why good error types attach context or messages as they travel; and propagation is not free of obligations - every layer that passes an error up must still clean up its own resources on the way, or you get a leak on the error path, which is one of the most common robustness bugs in C.

enum err { OK = 0, ERR_NOMEM, ERR_IO }; enum err load(const char *path, struct doc out) { FILE *f = fopen(path, "r"); if (f == NULL) return ERR_IO; struct doc *d = malloc(sizeof *d); if (d == NULL) { fclose(f); return ERR_NOMEM; } /* ...; */ *out = d; fclose(f); return OK; } - each failure returns a named code, and notice the fclose(f) before the ERR_NOMEM return so the error path does not leak the file.

A named error code travels up the call chain - and each layer cleans up as it returns.

Propagating an error does not excuse you from cleanup: a layer that returns a failure must still release the resources it acquired, or you leak on the error path. And a bare code far from its origin loses context, so attach it (which file, which call) where it helps.

Also called
passing errors up the call chainerror codes and enums錯誤傳播