Skip to content
academia.sh

Lesson 13 / 15

Error Handling Patterns

When a function fails, who frees the block it had already allocated up to that point is not a rule the compiler enforces — it is the function's own contract; the lesson measures this by running a return code, an error number, and a single-exit-point cleanup pattern.

Published Updated

In this lesson

  • Measures a function freeing its own allocated block on partial failure through a return code
  • Uses errno-based error reporting by printing only whether it is nonzero
  • Cleans up multiple resources with a single-exit-point goto cleanup pattern
Contents

The previous lesson took up four moments where the contract breaks, and never ran any of them. This lesson measures a moment where the contract does not break, but the function fails: a function starts allocating memory for a task, a condition partway through does not hold, and it has to return. The block allocated up to that point is now in nobody’s hands — who frees it follows not from a rule in the source, but from that function’s own contract.

The difference between these two lessons is subtle but binding: undefined behavior is the moment the contract breaks, partial failure is a moment the contract is still standing — the function fails, but that failure is not something the standard leaves undefined, it is a path the function itself anticipated. The difference carries over into the measurement too: what the previous lesson measured was a block that “no party could protect once the contract broke”; what this lesson measures is how the party that wrote the contract cleans up its own failure. The pattern proceeds in three stages: a single-resource error path, a multi-resource error path, and finally the standard library’s own error-reporting path.

Cleanup on the Error Path

We already saw this example in the measurement core: if the given name does not fit the limit parameter, record_create itself frees the record it had allocated up to that point and returns NULL to the caller. Nothing is left for the caller to clean up.

This rule may look like it applies to a single block only, but it is actually a promise a function makes to the outside world: a function that returns NULL commits to behaving as if it allocated nothing at all — the caller does not have to call record_destroy, because there is nothing to destroy. This promise is not marked by any keyword in the source; it is written only in the comment in owner.h, and record_create’s body keeps that promise. A function that does not keep the promise — for example one that allocates an intermediate block, returns NULL, and forgets to free that block — produces no warning in the language; the result is a silent memory leak that only shows up as a counter difference.

This section’s measurement is shown, together with the other two patterns, in the lesson’s single closing code block that contains main. The measurement core’s four files are used in this lesson exactly as before:

// counter.h — a wrapper that counts every allocation and every free
#ifndef COUNTER_H
#define COUNTER_H

#include <stddef.h>

void *count_alloc(size_t size);
void *count_grow(void *old, size_t size);
void count_free(void *block);
int allocated_count(void);
int freed_count(void);

#endif
// counter.c — the counters live only in this unit
#include <stdlib.h>
#include "counter.h"

static int allocated = 0;
static int freed = 0;

void *count_alloc(size_t size) {
    void *block = malloc(size);
    if (block != NULL) { allocated++; }
    return block;
}

void *count_grow(void *old, size_t size) {
    int new_block = (old == NULL);
    void *grown = realloc(old, size);
    if (grown != NULL && new_block) { allocated++; }
    return grown;
}

void count_free(void *block) {
    if (block != NULL) { freed++; }
    free(block);
}

int allocated_count(void) { return allocated; }
int freed_count(void) { return freed; }
// owner.h — the record's inside is hidden, the ownership contract is in the comments
#ifndef OWNER_H
#define OWNER_H

#include <stddef.h>

typedef struct Record Record;

/* The caller owns it and frees it with record_destroy. If the name does not
   fit the limit, returns NULL; the function frees what it allocated so far. */
Record *record_create(const char *name, size_t limit);

/* Frees the record together with the name buffer inside it. */
void record_destroy(Record *r);

/* The record owns it: the returned pointer is valid until the record is
   destroyed, and the caller does not free it. */
const char *record_name(const Record *r);

/* The caller owns it: the returned copy is freed with count_free. */
char *record_name_copy(const Record *r);

#endif
// owner.c — the unit where the contract is carried out
#include <string.h>
#include "owner.h"
#include "counter.h"

struct Record {
    char *name;
    size_t length;
};

Record *record_create(const char *name, size_t limit) {
    Record *r = count_alloc(sizeof *r);
    if (r == NULL) { return NULL; }

    r->length = strlen(name);
    if (r->length >= limit) {
        count_free(r);
        return NULL;
    }

    r->name = count_alloc(r->length + 1);
    if (r->name == NULL) {
        count_free(r);
        return NULL;
    }
    memcpy(r->name, name, r->length + 1);
    return r;
}

void record_destroy(Record *r) {
    if (r == NULL) { return; }
    count_free(r->name);
    count_free(r);
}

const char *record_name(const Record *r) { return r->name; }

char *record_name_copy(const Record *r) {
    char *copy = count_alloc(r->length + 1);
    if (copy != NULL) { memcpy(copy, r->name, r->length + 1); }
    return copy;
}

Single-Exit-Point Cleanup for Multiple Resources

record_create frees a single block itself because it allocates a single block. If a function allocates several blocks in sequence and one in the middle turns out invalid, the ones already allocated must first be rolled back. This pattern’s name is single-exit-point cleanup: the function jumps with goto to a single cleanup: label, and from that label frees everything allocated up to that point.

goto is generally a discouraged construct in C, because it can make control flow hard to follow. This pattern is the one accepted exception to that general advice: goto is used only forward, only to a single label at the end of the function, and only for cleanup. The alternative — repeated code that frees every block allocated so far at each individual failure point — does the same job, but its line count grows multiplicatively as the number of allocated blocks grows, and every new resource added requires updating every previous failure branch. goto cleanup instead gathers the rollback logic in a single place; adding a new resource only adds a new if and a new allocation line, the cleanup: label itself does not change.

// triple.h — declaration of a three-resource allocation
#ifndef TRIPLE_H
#define TRIPLE_H

#include <stddef.h>

int triple_alloc(size_t a, size_t b, size_t c, size_t limit,
                  int **p1, int **p2, int **p3);

#endif
// triple.c — rolling back on partial failure with the goto cleanup pattern
#include <stddef.h>
#include "triple.h"
#include "counter.h"

enum Status { STATUS_OK = 0, STATUS_ERROR = 1 };

int triple_alloc(size_t a, size_t b, size_t c, size_t limit,
                  int **p1, int **p2, int **p3) {
    int status = STATUS_OK;
    *p1 = NULL;
    *p2 = NULL;
    *p3 = NULL;

    if (a > limit) {
        status = STATUS_ERROR;
        goto cleanup;
    }
    *p1 = count_alloc(sizeof **p1);

    if (b > limit) {
        status = STATUS_ERROR;
        goto cleanup;
    }
    *p2 = count_alloc(sizeof **p2);

    if (c > limit) {
        status = STATUS_ERROR;
        goto cleanup;
    }
    *p3 = count_alloc(sizeof **p3);

    return status;

cleanup:
    count_free(*p3);
    count_free(*p2);
    count_free(*p1);
    *p1 = *p2 = *p3 = NULL;
    return status;
}

RO5. In the first call, b’s value (9) exceeds the limit (5), so with *p1 already allocated it will jump to the cleanup: label and that one block will be freed back; in the second call all three stay under the limit, so cleanup: is never reached, all three blocks pass to the caller, and the caller frees them itself. The single main below measures both of these paths in one run, together with record_create’s error path and reading errno.

triple_alloc’s cleanup: label only frees what was allocated up to that point — because the count_free function does nothing when called with a NULL pointer (the if (block != NULL) check in counter.c), the count_free(*p2) and count_free(*p3) calls safely no-op for the not-yet-allocated *p2 and *p3. This explains why the rollback pattern can always be written with the same order, the same three calls: the code does not have to track separately which block was actually allocated, because count_free already carries the guarantee “do nothing if not allocated”. It is count_free that carries the guarantee, not goto — goto only gathers the flow at a single point.

Return Code and Error Number

triple_alloc returns an int, and that value answers only the question succeeded or failed; it does not say which resource was rejected — if that information is wanted, it has to travel through a separate parameter. The standard library’s own error reporting instead works through errno: when a function fails it sets errno to a nonzero value, but that value’s own contents are target-dependent and are never printed; only whether it is nonzero is read.

A return code and errno are two different solutions to the same problem, but they do not operate at the same layer. A return code is that function’s own design — triple_alloc‘s int return is a contract chosen by whoever wrote that function, and shares no common rule with any other function’s return value. errno, on the other hand, is the standard library’s shared channel: strtol, file operations, and memory allocation all use the same errno variable — which is why telling apart which call left which error requires resetting it immediately before every call.

// measurement.c — error path, goto cleanup, and errno measured in one run
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include "owner.h"
#include "counter.h"
#include "triple.h"

static int diff(void) {
    return allocated_count() - freed_count();
}

static void observe(const char *name, const char *result) {
    printf("%-28s%s\n", name, result);
}

int main(void) {
    char buffer[64];

    printf("%-28s%s\n", "observation", "result");

    /* --- cleanup on error path: record_create frees its own allocation --- */
    int before = diff();
    Record *too_long = record_create("a very long record name", 8);
    snprintf(buffer, sizeof buffer, "null %d, counter diff %d", too_long == NULL, diff() - before);
    observe("cleanup on error path", buffer);

    /* --- goto cleanup: rolling back on partial failure --- */
    int *a = NULL, *b = NULL, *c = NULL;
    before = diff();
    int status = triple_alloc(2, 9, 2, 5, &a, &b, &c);
    snprintf(buffer, sizeof buffer, "status %d, counter diff %d", status, diff() - before);
    observe("second resource rejected", buffer);

    before = diff();
    status = triple_alloc(1, 2, 3, 5, &a, &b, &c);
    count_free(a);
    count_free(b);
    count_free(c);
    snprintf(buffer, sizeof buffer, "status %d, counter diff %d", status, diff() - before);
    observe("all three accepted", buffer);

    /* --- errno: only whether it is nonzero is read --- */
    errno = 0;
    char *end = NULL;
    long overflowed = strtol("99999999999999999999", &end, 10);
    (void)overflowed;
    observe("overflowing number parse", errno != 0 ? "error number nonzero" : "error number zero");

    errno = 0;
    long normal = strtol("42", &end, 10);
    (void)normal;
    observe("normal number parse", errno == 0 ? "error number zero" : "error number nonzero");

    return 0;
}
observation                 result
cleanup on error path       null 1, counter diff 0
second resource rejected    status 1, counter diff 0
all three accepted          status 0, counter diff 0
overflowing number parse    error number nonzero
normal number parse         error number zero

RO6. When the r->length >= limit check inside record_create holds, the function frees its own allocated r with count_free and returns NULL; the counter difference coming out as 0 is proof that the one allocated block was freed within the same function. Before each of the two strtol calls, errno is explicitly reset to zero: errno is only set when there is an error, it is not guaranteed to be reset on success — resetting it before the check is required so as not to read a value left over from a previous call.

None of the five rows shown in the table continues from another; each starts independently, with its own before/diff pair or its own errno reset. This separation is deliberate: running two measurements back to back and interpreting the difference by reading a single counter could confuse which measurement caused which change. Calling the diff() function before each observation — the before = diff(); line — prevents this confusion: each row’s counter difference comes only from that row’s own call, not from the cumulative effect of the ones before it.

The Bounding Measurement

Who frees what was allocated on partial failure must be written separately in every function. record_create frees its own allocation; so does triple_alloc — but this is not a language rule, it is a contract those functions chose for themselves. Another function could return NULL without freeing what it allocated on partial failure, and the compiler would not stop it; the contract is written only in the comment in the header file, and the caller has to read it.

This also draws the boundary of what the lesson measures: we showed that record_create and triple_alloc clean up their own failure paths correctly, but that these two being written correctly is not proof that some other function will follow the same discipline. The same question has to be asked again for every new function: what was allocated on partial failure, who will free it, and where is that contract written? The measurement stops here; this is a lesson that runs one instance of the contract, not a tool that checks whether the contract exists.

This boundary applies the same way to errno: the standard guarantees that a function will set errno on failure, but it does not guarantee which functions may overwrite it without reading it first. The only way to observe the difference is to reset errno immediately before every call and read it immediately after — if another library function runs between the two calls, that function may change errno even if it succeeds. Both patterns measured in this lesson (goto cleanup and resetting errno) rest on the same principle: the language never performs any cleanup or any reset on its own; the party that does it is always the line of code itself.

Summary

  • A function may free the block it allocated on partial failure itself (record_create), or return without freeing it; which one happens is the function’s own contract.
  • Single-exit-point cleanup (goto cleanup) is the standard pattern for rolling back multiple resources in sequence, and it brought the counter difference to 0 at both failure points.
  • errno is only read for whether it is nonzero; neither its numeric value nor a message text was ever printed in any observation.
  • The memory this lesson measures is in the callee class: the module itself took on cleanup on the error path, leaving nothing for the caller.
  • All three patterns share one point: the language provides none of them on its own — whoever writes the goto cleanup label, guarantees that count_free no-ops on NULL, and resets errno before every call, it is always the programmer’s own lines.

Next Step

While cleaning up on the error path, record_create also rejected something: a given name not fitting the limit. That limit — the limit parameter — does not come from the source, it is given by the caller. The next lesson puts that parameter at the center: how giving the length to the caller prevents a buffer overflow.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close