The first failure taught the failure channel at a call site. Generators taught a body that suspends between values. This page is where the two meet, and the meeting has a puzzle in it: when a suspended generator runs fail, there is no call statement to deliver to — the body is frozen mid-conversation, and its real call site is the consumer’s loop. Mica’s answer: the failure rides the channel the body already answers through, and the loop carries the consumption clause.

The example is examples/FallibleStreams in the tutorial repository.

Getting the file

make -C examples/FallibleStreams run
FallibleStreams

pull chain, handled at the loop
  reading 10
  reading 20
  reading 30
  producer: cleanup ran
  consumer: the sensor saturated

propagated by the summing function
  producer: cleanup ran
  consumer: saturated after summing what arrived
  sum answered -1

ring, handled at the same kind of loop
  producer: cleanup ran
  reading 10
  reading 20
  consumer: the sensor saturated, across the ring

The example, walked

The signature, on a generator

type
    SensorError = error (Saturated);

generator Readings(limit : int64) : stream of int64 fails SensorError;
begin
    defer WriteLn("  producer: cleanup ran");
    ...
    fail Saturated;

fails means on a generator exactly what it meant on a function: this body can end on the failure channel, and the type says so. The defer is the proof line for the order this page cares about.

The loop carries the clause

for v in Readings(3) do
begin
    WriteLn("  reading %lld", v);
end
on fail e do
    if e = Saturated then
        WriteLn("  consumer: the sensor saturated");

The consumption clause sits on the loop, after its end, and offers exactly the choices a call statement offers: a handler binding the code, on fail leave forwarding it on the consumer’s own channel — the summing function in the example is that road — and on fail continue discarding it visibly. Read the first section’s output order: the three readings, then the producer’s cleanup, then the consumer’s handler. The failure unwound the producer first — its deferred statements ran on the way out — and only then surfaced at the pull site. Cleanup and error handling are one mechanism, in the streams world too.

The ring carries the same word

s : stream of int64 fails SensorError;
...
s := schedule Readings(2) buffer 4;

Schedule the same fallible body behind a bounded ring and the story holds: the failure crosses the ring as the stream’s ending and surfaces at the same kind of loop with the same clause. One detail is worth reading off the output: in the ring section the cleanup line appears before the drained readings. The buffered producer ran ahead, failed, and unwound while values still sat in the ring — the failure waits in line behind them, and the consumer hears it exactly once, after draining what was honestly produced.

And note the variable’s type: stream of int64 fails SensorError. A plain stream of int64 cannot bind a fallible generator — the compiler refuses it and spells the fix, because a failure that a type quietly dropped would be a failure nobody consumes.

Try it

1. Drop the domain from the variable. Change the declaration to s : stream of int64; and rebuild:

analyzer error 5388: the fallible generator 'Readings' cannot bind the stream variable 's', whose type carries no fails domain: declare the variable over the generator's own domain — 'stream of T fails SensorError' — and the for-in over it consumes the failure exactly as the direct call road does

2. Leave early. In the first loop, add leave; after the first reading prints. One reading, then the producer’s cleanup — and no handler line, because a consumer that abandons a fallible stream consumes no failure: cancellation routes the body onto a plain leave, and only an executed fail ever records one. Early exit is not an error, and the arrangement knows the difference. (Bare leave ends the whole main block, as init-and-leave teaches — the later sections go with it, which is why this is an experiment and not the example.)

What the compiler proved

A fallible stream is fallible in its type, at the generator, at the variable, and at the loop — none of the three can quietly disagree. The producer’s cleanup runs before any consumer hears the failure, on the pull chain and across a ring alike. And the loop’s clause is the call statement’s clause: one failure grammar, everywhere a failure can arrive.

Next

Channel streams for the rest of the push-side story, and Error domains for failure types richer than one code. Every example lives in the tutorial repository.