Chapter 8: Error Handling
SafeC has no exceptions. There's no throw, no catch, no
stack-unwinding machinery running behind your back — a function that can
fail says so in its return type, and the caller is required to deal with
that possibility explicitly. This chapter covers the three pieces that
make that workable in practice: ?T optionals for signaling absence,
try for propagating failure without a pyramid of if checks, and
defer/errdefer for cleanup that runs no matter how a function exits.
?T: might not have a value
You've seen ?T and null already, briefly, in earlier chapters — this
is where they get their proper introduction. ?T means "a T, or
nothing":
?int parse_positive(const &char s) {
int v;
unsafe { v = atoi(s); }
if (v <= 0) {
return null;
}
return v;
}
A plain T returned from a ?T-returning function implicitly wraps to
"present" — return v; above needs no explicit "wrap this as present"
call. null is the one spelling of "absent," for both ?T optionals and
the nullable references from Chapter 5
— there's no separate some(x)/none constructor you call in ordinary
code; those two names only exist as match patterns, the same
dot-free null/some(x) shape a nullable reference match uses.
Reading a ?T back out requires going through one of a small set of
sanctioned operations — direct access isn't allowed, the same restriction
Chapter 5 placed on nullable
references, and for the same reason: an "I forgot to check" bug here is
exactly the null-pointer-dereference class of bug SafeC is trying to
design out entirely.
match (parse_positive("21")) {
case none: printf("nothing\n");
case some(x): printf("got %d\n", x);
}
try: propagate failure without the pyramid
Chaining several functions that can each fail tends to produce, in
languages without try, a staircase of nested "if it worked, do the next
thing" checks. try flattens that: it unwraps a ?T, and if the value
was null, immediately returns null from the enclosing function
too — one line instead of a nested if:
?int double_positive(const &char s) {
int v = try parse_positive(s); // if parse_positive returned null,
// double_positive returns null here too
return v * 2;
}
double_positive("21"); // some(42)
double_positive("-5"); // none -- parse_positive's null propagated straight through
This is the same shape as Rust's ? operator or Go's if err != nil { return err } idiom, minus the boilerplate either of those needs at every
call site — try is the boilerplate, spelled as a single keyword.
defer: cleanup that always runs
defer schedules a statement to run when the enclosing scope exits — by
falling off the end, by an early return, whatever the path — in
last-in-first-out order:
void demo() {
printf("start\n");
defer printf("first deferred\n");
defer printf("second deferred\n");
printf("middle\n");
}
start
middle
second deferred
first deferred
This is the idiomatic way to pair an acquisition with its release right
next to each other in the source — malloc/free, fopen/fclose,
a mutex lock/unlock — instead of a release statement duplicated at every
early-return path, which is exactly the kind of thing that's easy to
forget at one of them and hard to notice you forgot.
errdefer: cleanup only on the failure path
errdefer is defer's narrower sibling — written the same way, but
runs only when the function is exiting because a try propagated a
failure out of it, not on an ordinary return (successful or
otherwise — a plain explicit return null; doesn't trigger it either,
only a try that actually unwinds through this function does):
?int risky_step(int fail_at) {
if (fail_at != 0) { return null; }
return 0;
}
?int process(int fail_at) {
printf("processing\n");
errdefer printf("rolling back\n");
int r = try risky_step(fail_at); // propagates risky_step's failure via try
return 42;
}
process(0) prints just processing — risky_step succeeds, try
unwraps it normally, and errdefer never fires. process(1) prints
processing then rolling back — risky_step returns null, try
propagates that failure out of process (making process(1) itself
evaluate to null), and that unwind is what fires the errdefer.
Beyond ?T: richer errors
?T tells you that something failed, not why — fine for parse_positive,
where "not a positive number" is the only failure mode worth naming, less
fine for something like a file-open call that might fail for several
distinct reasons a caller genuinely needs to tell apart. For that,
reach for std::Result (built on the same tagged-union mechanism Chapter
7 taught you to build by hand) — see
Standard Library once you need it; this chapter's ?T is
still the right default for the common "did it work or not" case.
Next: A Final Project — everything from the last eight chapters, combined into one program worth actually keeping around.