Split successes from failures
Requirement
Seven orders from the 2026-08 import go through an async validation
that throws for two of them (a missing shipping address, an unknown
SKU). Keep both outcomes: print an ok:
line per valid order, then the failure count. The data is in the code;
both versions must print the lines shown under Expected output.
Expected output
ok: order #1001 ok: order #1002 ok: order #1004 ok: order #1005 ok: order #1007 failed: 2
Side by side
RxDart
FxDart
Why they differ
A stream has two channels — data and error — and the error channel has
whole-stream semantics: one thrown validation would kill the pipeline
with five orders still unprocessed. So the RxDart side cannot simply
asyncMap(validate); it wraps each validation in
its own inner stream (Rx.fromCallable), catches on that
inner error channel with onErrorReturnWith, and re-encodes
the failure as a data value before merging back. The recovery works,
but it is channel plumbing: the error had to leave the data path just
to be escorted back in.
The FxDart side never puts failures on a separate channel. One
try/catch inside map turns each outcome into a plain
record — (id, error?) — and from there
partition is an ordinary predicate split. This is the
pull model's general stance on errors: they are values that
flow through the same typed pipeline as everything else, so keeping
successes and failures together costs nothing. When outcomes are part
of the result rather than an interruption, the model without a
privileged error channel has less to undo.
Benchmark
Async case: the headline scale is N = 10,000, not 1,000,000. Every element costs an event-loop turn on both sides, so a million real awaits would measure Dart's event loop for minutes — not the pipeline. Delays are zero-length and the example's concurrency limit is kept; what the bars compare is the pipeline machinery.
N = 100
Time Tie
Peak memory Tie
N = 10,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.