Report every validation error
Requirement
Five signup forms from the 2026-08 batch are checked against three
rules (name present, email contains @, age 18+). A form
can break several rules — report every broken rule
per form, then list the forms that passed. The data is in the code;
both versions must print the lines shown under Expected output.
Expected output
form #2: name is required; email is malformed; must be 18 or older form #4: email is malformed form #5: name is required; must be 18 or older valid: ana, cy
Side by side
RxDart
FxDart
Why they differ
Accumulating validation is the job the Rx error channel is structurally
unable to do. A stream error carries exactly one object, and emitting
it ends the stream — raise the first broken rule and neither the form's
other failures nor the remaining forms are ever seen. Every recovery
operator (onErrorReturn, onErrorResumeNext)
is built for that one-error-then-done shape. So the working RxDart
version shown here quietly abandons the error channel: it maps each
form to a record of its failures on the data channel — at
which point the stream is contributing an async main and
a toList, nothing more.
The FxDart side is the same idea without the wrapper: errors are plain
values, so a synchronous map + partition
yields the failed forms (with all their errors) and the valid
ones in one expression. And because FxDart treats errors as values
throughout, this pattern scales past records: the typed-errors layer
accumulates for you — zipOrAccumulate runs every rule and
concatenates the failures into a NonEmptyList, and
mapOrAccumulate validates a whole collection fail-slow.
See the accumulate tutorial for that full version; the
stream model has no counterpart to reach for.
Benchmark
N = 100
Time Tie
Peak memory RxDart wins
N = 1,000,000
Time FxDart wins
Peak memory RxDart wins
Bars are medians of repeated timed iterations in fresh processes per side (small N is batched for timer resolution). Sides within 5% of each other — or within 0.6 ms, a difference no person can perceive — count as a tie; close relative races are re-measured up to 5 runs. In an app, anything under a few milliseconds is invisible to the user regardless of which bar is shorter. Memory is peak process RSS. The Dart VM and the dataset are identical on both sides, so the difference between the two bars is what the pipeline itself holds onto.