Skip to main content

Errors Through Layers

intermediate17 min readLesson 135 of 148

Propagation that adds context: wrap, don't swallow; distinguish, don't conflate; and the two-phase init that makes construction failable.

Swallowing is the original sin

int load_config(Config *c) {
    if (read_file(c) != 0) return 0;      /* failed, but reports success */
    return 0;
}

A layer that converts failure into success doesn't remove the failure โ€” it hides it, so the bug surfaces three layers away from its cause. The opposite sin is conflation: returning the same code for "file missing" and "file unreadable", so the caller cannot react differently. The middle path is wrap and add context:

/* layer 1 returns: 0 ok, -1 io error */
/* layer 2 adds meaning without lying: */
int load_app(App *app) {
    if (load_config(&app->cfg) != 0) return CFG_ERR;     /* distinct code */
    if (load_assets(&app->art) != 0) return ASSET_ERR;   /* distinct code */
    return OK;
}

Each layer translates the codes below it into codes that mean something at its own abstraction level. The caller of load_app does not care which file read failed โ€” it cares that configuration, specifically, is broken.

Construction can fail: two-phase init

Constructors in C cannot return errors inside the object โ€” so the idiom is either NULL-returning constructors or two-phase init:

/* phase 1: make it valid-but-empty; returns error code */
int conn_init(Conn *c);
/* phase 2: attach resources; returns error code */
int conn_connect(Conn *c, const char *target);
/* teardown is always safe, from any state */
void conn_destroy(Conn *c);

The rule that makes it work: destroy must be safe from every reachable state โ€” freshly-init'd, connected, or failed-connect. That invariant is testable: init, destroy; init, connect-fail, destroy; init, connect, destroy โ€” no leak, no crash in any order.

The panic boundary

Some failures are not recoverable by the layer that sees them: a NULL self argument is a contract violation by the caller โ€” a programming error, not a runtime condition. Real APIs either crash fast (assert) with a message naming the file/line, or document the UB honestly. Silently "handling" a NULL self hides the bug from the only person who can fix it: the programmer who made the call.

Now practice

Layered Errors GymTwo-phase init with failable construction, and error codes that wrap without lying.1 challenge ยท ยท ~26 min