VIP-20: iterating a Stream with for try
Status: accepted by Will, 2026-10-28.
Decision
for try (const row of rows) {
if (row.skip) continue
if (row.last) break
try save(row)
}
// exactly this Effect TS
yield* Stream.runForEachWhile(rows, (row) => Effect.gen(function* () {
if (row.skip) return true
if (row.last) return false
yield* save(row); return true
}))-
for try (const x of stream)consumes a Stream inside aneffectfunction or block, pulling one element at a time, in order. It mirrorsfor awaitover AsyncIterables, which keeps its JavaScript meaning (VIP-5). -
continueskips one element, andbreakstops consuming.returnfrom the enclosing function is a compile error. A failure in the body fails the loop. -
Ambiguity: none.
tryis a reserved word, and JavaScript'sforcan only be followed by(orawait. -
Amended 2026-10-29: bindings and operands. The head takes every
ForBindingthatfor…oftakes (const,let, destructuring), minusvar. The operand is aStreamonly; an AsyncIterable is a compile error that points toStream.fromAsyncIterable. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 48). -
Amended 2026-10-29: no labeled jump out. A labeled
breakorcontinuefrom afor trybody to a statement outside it is a compile error, asfor concurrentalready rules. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 49). -
Amended 2026-10-30: source head scope and item checks. Evaluate the operand in ECMAScript's separate lexical environment with uninitialized head bindings. RHS closures retain those TDZ cells; each item receives fresh initialized bindings and the body keeps its own block. Apply the loop's outward-jump restrictions to head binding initialization and body, including defaults and computed keys, while nested source functions keep their own targets. The RHS retains the enclosing activation's permitted outward completions. Preserve the native per-element true/false protocol after the item completes; see Running Effects.
-
Amended 2026-10-30: plain forks use the current consuming fiber.
Stream.runForEachWhileruns the item callback on that fiber, without creating a per-item owner. A plainforkchild can outlive an item and successful loop completion while that owner continues; it is not implicitly joined and is interrupted when the owner terminates. The consuming fiber may itself be another child or a concurrent item's fiber. Explicit fork-owner forms andStreamfinalization keep their native behavior, without a new start-order guarantee or item scope. See Running Effects and the ownership witness.
History
About 9 findings, mostly about producers, which have their own VIP. The round-11 major, where a type decided whether for…of consumed a Stream, doesn't apply: for try is syntax.
Prior art
JavaScript's for await; Kotlin's flow.collect { }; Rust's while let Some(x) = s.next().await.
Will's words
"good recomendation"