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. errnois 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 cleanuplabel, guarantees thatcount_freeno-ops onNULL, and resetserrnobefore 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.