prefer_chaining_over_intermediate_run
This rule flags a function body that calls .run() on two or more fpdart pipelines instead of chaining them and running once.
Why use this rule
Section titled “Why use this rule”flatMap carries the error channel through the whole pipeline. Any step that fails short-circuits the rest, and the failure handling is written once, at the fold — the compiler enforces what the manual version leaves to discipline.
Running each step separately throws that away. Every intermediate result has to be unwrapped by hand, every failure branch rebuilt, and the nesting grows one level per step. It is also where failures get quietly lost: one forgotten if and the body continues with a value that was never produced, or a rebuilt Left swallows the original error.
That imperative shape is the exact thing TaskEither exists to delete. A body with several .run() calls is usually a chain that was never joined up.
See also: Why chaining beats manual result handling
Future<Either<Failure, Deal>> best() async { final area = await getArea().run();
if (area case Right(value: final a)) { final restaurant = await getRestaurant(a.id).run();
return switch (restaurant) { Right(value: final r) => await getDeal(r.id).run(), Left() => Left(const RestaurantFailure()), // original failure lost }; } return Left(const AreaFailure());}TaskEither<Failure, Deal> best() => getArea() .flatMap((area) => getRestaurant(area.id)) .flatMap((restaurant) => getDeal(restaurant.id));Flat regardless of step count, no manual unwrapping, and the original failure propagates untouched. Run it once at the boundary that renders the outcome.
When the chain grows past two or three steps, Do notation reads better than nested flatMap.
Known limitations
Section titled “Known limitations”A .run() inside a closure is not counted against the enclosing member. A callback passed to a widget or an event handler legitimately runs its own pipeline, and counting those would report a body that is already correct.
The rule reports the member’s name rather than each .run(), because the fix is to restructure the body as one chain — a single edit at that level. Reporting every call would suggest each one is separately wrong.
No quick fix is offered: joining the steps means rewriting the body’s control flow, including deciding what each unwrapped branch was for. That is a change to read, not to apply blind.
Options
Section titled “Options”many_lints: rules: prefer_chaining_over_intermediate_run: min_sequence: 3rules: prefer_chaining_over_intermediate_run: min_sequence: 3| Option | Type | Default | Description |
|---|---|---|---|
min_sequence |
int | 2 |
How many .run() calls a body may contain before it is reported. 1 reports every body that runs a pipeline at all, which suits a codebase that folds exclusively at the notifier boundary |
Configuration
Section titled “Configuration”This rule is in the opinionated preset. With a lower preset, enable it by
name with prefer_chaining_over_intermediate_run: true.
To turn it off:
rules: prefer_chaining_over_intermediate_run: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Related rules
Section titled “Related rules”avoid_ad_hoc_left_type— A pipeline only composes when every step shares one error type.avoid_bare_await_in_do— Awaiting a raw Future inside a Do block escapes the block’s tracking.avoid_dollar_outside_do_frame— Calling a Do block’s extraction function from a nested callback unwinds through code that cannot handle it.avoid_either_of_future— A Future nested in Either or Option escapes the error channel.