Skip to content

avoid_chain_first_swallowing_failure

v1.2.0WarningFixConfigurablefpdart

This rule flags chainFirst on an Either, TaskEither or IOEither. fpdart’s chainFirst ignores the failure of the effect it runs.

fpdart declares chainFirst like this:

TaskEither<L, R> chainFirst<C>(TaskEither<L, C> Function(R b) chain) =>
flatMap((b) => chain(b).map((c) => b).orElse((l) => TaskEither.right(b)));

The trailing orElse turns a failing effect back into success, and the pipeline carries on with the original value. The name reads like “run this step too”, so it gets used for checks and writes. A permission check chained this way cannot reject anything, and a save chained this way can fail without anyone noticing.

flatMap with an inner map keeps the value and lets the failure through. Keep chainFirst only where ignoring the effect’s failure is what you want, such as a best-effort log line.

See also: fpdart: TaskEither.chainFirst

import 'package:fpdart/fpdart.dart';
TaskEither<String, int> loadUser(int id) => TaskEither.right(id);
TaskEither<String, Unit> saveUser(int user) => TaskEither.left('disk full');
// Succeeds even though saveUser failed.
TaskEither<String, int> save(int id) => loadUser(id).chainFirst(saveUser);
import 'package:fpdart/fpdart.dart';
TaskEither<String, int> loadUser(int id) => TaskEither.right(id);
TaskEither<String, Unit> saveUser(int user) => TaskEither.left('disk full');
// Fails with 'disk full'.
TaskEither<String, int> save(int id) =>
loadUser(id).flatMap((user) => saveUser(user).map((_) => user));

The fix rewrites x.chainFirst(effect) to x.flatMap((value) => effect(value).map((_) => value)). It handles a tear-off (saveUser, repo.save) and a lambda with an expression body ((user) => saveUser(user)). A block-bodied lambda, explicit type arguments or a callback computed by an expression are left for you to rewrite.

The fix changes behaviour on purpose: after it, a failing effect fails the pipeline. It is applied one location at a time, never in bulk.

Every chainFirst on these types is reported, including the ones where dropping the failure was intended. Suppress those with an // ignore: comment that says why.

Files under test/ are skipped by default. Set ignore_tests: false to report them too.

analysis_options.yaml
many_lints:
rules:
avoid_chain_first_swallowing_failure:
ignore_tests: false
Option Type Default Description
ignore_tests bool true Skip files under test/

This rule is in the pedantic preset. The “Convert to chainFirst” assist offers chainFirst deliberately, so a codebase may have chosen it. Turn it on explicitly:

many_lints.yaml
rules:
avoid_chain_first_swallowing_failure: true

To keep the rule on but skip certain paths, use per-rule exclude.