Cleanup code has a placement problem. The close belongs at the end of the function, but the open that requires it is at the top — thirty lines apart, with every early return in between obliged to remember. C solves it with goto cleanup and discipline. C++ solves it with destructors and a book of rules about them. Go introduced defer and got the placement right: write the cleanup next to the thing that needs cleaning, and let the exit run it.

Mica has defer, and this series has been using it since the first task cancelled. What it has not done is state the rules — and defer is only a mechanism if its rules are exact. There are four.

The example is examples/Defer. Build and run it:

make -C examples/Defer run

Rule one: newest-first

WriteLn("  acquire a");
defer WriteLn("  release a");

WriteLn("  acquire b");
defer WriteLn("  release b");

WriteLn("  body runs between");
  acquire a
  acquire b
  body runs between
  release b
  release a

Teardown mirrors setup — b released before a, because b was acquired after a — and nobody wrote that order. Each defer stacks on the one before it, and the exit unwinds the stack. If b’s acquisition depended on a (a file in a directory, a lock under a lock), the release order is the only correct one, and it fell out of the source order for free.

Rule two: exit-time values

done := 0;
defer WriteLn("  finished %lld items", done);

done := 3;
WriteLn("  worked");
  worked
  finished 3 items

The deferred statement printed 3, not 0. A defer registers the statement, not a snapshot of its operands: when it runs at the exit, it reads the values that hold there. That is what makes the outcome-reporting idiom possible — one line, registered early, describing what the body eventually did. You saw it in the carriers article:

defer if ScopeTimedOut() then WriteLn("timed out") else WriteLn("completed");

The query runs at the exit, after the drain — which is the only moment it could answer correctly.

Rule three: an early leave runs what it reached

defer WriteLn("  cleanup one");

if stop then
begin
    WriteLn("  leaving early");
    leave;
end;

defer WriteLn("  cleanup two");
WriteLn("  ran to the end");
  leaving early
  cleanup one
  --
  ran to the end
  cleanup two
  cleanup one

On the early path, only cleanup one ran — the second registration stands below the leave, so on that path it never happened. On the full path, both ran, newest-first. A defer is not “code that runs at exit”; it is a registration statement, and like any statement, paths that do not execute it do not get its effect.

This is why placement is checked. A defer must stand unconditionally in its body:

for i := 1 to 3 do
begin
    defer WriteLn("deferred %lld", i);
end;
analyzer error 5192: defer statement must be placed unconditionally in the
body of its function, but found it inside one 'for statement'

A per-iteration registration would either accumulate unboundedly or mean something loop-shaped that defer does not mean — so the question is closed at compile time, and cleanup that belongs to one iteration belongs in a routine the iteration calls, which is where its resources belonged anyway.

Rule four: loop exits are not activation exits

defer WriteLn("  the activation's cleanup, last");

scan : for round := 1 to 5 do
begin
    WriteLn("  round %lld", round);
    leave scan when round = 2;
end;

WriteLn("  after the loop, activation still alive");
  round 1
  round 2
  after the loop, activation still alive
  the activation's cleanup, last

The targeted leave scan ended the loop. The activation lived on — the post-loop line proves it — and the deferred statement waited for the real exit. Registrations belong to activations, never to loops: a leave that exits a loop fires nothing, a leave that exits the activation fires everything reached, and the two spellings make the difference visible at the exit site.

One mechanism, the whole language

The reason defer deserved the last article of this series is that you have already seen it everywhere, and it was the same defer each time:

  • The heap: defer dispose scratch is road two of the obligation ledger — and the analysis knows a deferred dispose discharges the debt on every path.
  • Generators: an abandoned body is resumed once with its cancellation set, and its deferred statements runthe cleanup promise that made instances safe to freeze and drop.
  • Tasks: a cancelled task unwinds like any early exit, so its defers run — cancellation without a special cleanup path.
  • Scopes and drains: until Pool items drain at the owner’s exit, interleaved with the owner’s own defers in registration order.

Exit paths in Mica come in many spellings — fall-through, leave, a fail on the failure channel, a cancellation delivered at a suspension point — and there is exactly one kind of exit as far as cleanup is concerned. That is the design: not that defer exists, but that nothing else does.

What this does not do

No per-iteration defer. Shown above, refused as 5192. A loop’s cleanup lives in the routine the loop calls.

A deferred statement with an until owner must be a dispose. defer until is the heap’s owner-binding road, not a general at-exit hook for arbitrary code on someone else’s activation:

analyzer error 5197: a deferred statement with an 'until' owner must be a
dispose statement

No program-exit hook in a program body. A library may register finalizers that run at program end; a program’s own last breath is its final statements. What runs at every activation exit is bounded and visible in that activation’s source.

defer does not catch failures. A deferred statement runs during an unwind; it does not stop one. Consuming a failure is on fail’s job — the error-handling material — and the two compose: the defer runs, then the failure continues to its handler.

What the compiler proved

Teardown mirrored setup without an ordering in the source. A cleanup line reported a value its registration had never seen. Two exit paths each ran exactly the registrations they reached. A loop ended without firing an activation’s cleanup. And the one shape that would have made registrations unbounded was refused at compile time.

The series, complete

This was the fourteenth pair of the advanced series, and the last. The full path now runs: valuesaggregatesdynamic arraysconformant arrayssetsmemory classesthe heapownershipthe proofs → defer → tasksgeneratorscarriersdata races. Every example compiles and runs in the tutorial repository, on both architectures, today.