Note: This post has been updated 17-Nov-2018 to reflect the current state of the rules.
Loops return null if-and-only-if they break
One very good reason this rule exists is to make it easy to build custom loop constructs out of several other loop constructs. You can tell “from the outside” whether one of your sub-loops was BREAK’d…this way the the higher level construct is aware it shouldn’t run any more of its component loop phases.
(If this rule did not exist, then implementing a loop out of several other loops would have to invasively hook and rebind BREAK in each loop body to its own, and handle that. Even if it were possible–which it probably should be–this would be complex and inefficient. So the simpler rule is better!)
That’s the only “hard rule” of loop return results, however…
…many common loops return a BLANK! if the body never ran
>> loop 0 [<unreturned>] == _ >> for-each x  [<unreturned>] == _
To distinguish this case (and the breaking case) from normal running, such loops also “voidify” a body result of blank or null:
>> for-each x [1 2 3] [null] // #[void] >> loop 1 [_] // #[void] >> x: true while [x] [x: false] // evaluating to logic false still works == #[false] // so you can get a "falsey" result even if the body runs to completion
Remember that voids may be cantaknerous values, but they are nonetheless "ANY-VALUE!"s…hence they will trigger THEN and not trigger ELSE. But if you try to use void with conditional logic and test it for truth or falsehood, you’ll get an error.
Reacting to BREAKs and empty loops is easy!
Loop aggregators aren’t the only place that benefits from being able to tell what happened with a loop from its result. Plain user code reaps the benefits as well.
Right off the bat, if your loop body always returns a truthy thing, you can leverage the result to make sure at least one body ran and there wasn’t a break:
all [ for-each x block [ if some-test x [break] <truthy-result> ] // ^-- falsey to interrupt ALL if block is empty, or BREAKs ... ]
If you don’t want to count not having the body run as an equivalent result to a BREAK in a situation like this, leverage the fact that BLANK! is a value to trigger THEN and truthyify that case:
all [ for-each x block [ if some-test x [break] <truthy-result> ] then [true] // ^-- only on BREAK is it now falsey to interrupt the ALL ... ]
If you’re only concerned with whether a loop BREAKs, then ELSE is the ticket:
for-each x block [ if some-test x [break] <truthy-result> ] else [ // This code runs only if the loop breaks // ...so it still runs even if block is  ]
You can combine that with THEN to segregate code when the loop doesn’t break:
for-each x block [ if some-test x [break] <truthy-result> ] then [ // This code runs only if the loop doesn't break ] else [ // This code runs only if the loop breaks ]
You can use AND and OR to test the logic cases:
for-each x block [ if some-test x [break] <truthy-result> ] or [ // This code runs if the loop breaks -or- DIDN'T // run the body at least once ] for-each x block [ if some-test x [break] <truthy-result> ] and [ // This code runs if the loop doesn't break and ran // the body at least once ]
Here’s a very cool real world case from the console code:
pos: molded: mold/limit :v 2048 loop 20 [ pos: next (find pos newline else [break]) ] then [ insert clear pos "..." ]
You have up to 2048 characters of data coming back from the mold, ok. Now you want just the first 20 lines of that. If truncation is necessary, put an ellipsis on the end.
loop 20 obviously will always try and run the body at least once. (So the loop will never return blank here.)
FIND will return NULL if it can’t find the thing you asked it, so the ELSE runs when you can’t get the position. If it makes it up to 20 without breaking, then the THEN clause runs.
So there you go. The first 20 lines of the first 2048 characters of a mold, truncating with “…” I think the THEN really follows the idea of completion, it makes sense (to me) that a BREAK would bypass a THEN (or an ALSO, which is similar) clause.
I encourage people to get creative, to look at ways to use this to write clearer/shorter/better code.