Skip to content

avoid_throw_in_fp_callback

v1.0.0WarningConfigurablefpdart

This rule flags a throw inside an fpdart Do body or inside a callback passed to one of fpdart’s chaining methods (map, flatMap, chainEither, mapLeft, andThen, alt, orElse).

fpdart’s whole premise is that failure travels in the value — the Left of an Either, the None of an Option. That is what lets a caller handle every failure in one fold, and what makes the compiler enforce it.

A throw inside map or a Do body leaves that channel. It escapes the pipeline as an ordinary exception, so a caller that carefully folds every failure still crashes. Worse, the signature keeps promising otherwise: TaskEither<Failure, T> says the only failures are Failures.

This is one of four Do pitfalls that fpdart documents in its own do_constructor_pitfalls example.

See also: fpdart: Do notation, fpdart: Either

The signature promises the only failure is a String, but the throw leaves the error channel entirely — a caller that folds every Left still crashes:

TaskEither<String, int> parseAge(TaskEither<String, String> raw) =>
raw.map((value) {
final age = int.tryParse(value);
if (age == null) {
throw FormatException('not a number: $value'); // escapes the pipeline
}
return age;
});

Put the failure in the value, where the signature already says it is:

TaskEither<String, int> parseAge(TaskEither<String, String> raw) =>
raw.flatMap(
(value) => switch (int.tryParse(value)) {
final age? => TaskEither.of(age),
_ => TaskEither.left('not a number: $value'),
},
);

When the throwing code is someone else’s, catch it at the boundary and map it in once:

TaskEither<String, String> load(Future<String> Function() request) =>
TaskEither.tryCatch(request, (error, stackTrace) => '$error');

Throwing UnimplementedError, UnsupportedError, StateError or AssertionError is allowed by default. Those mark a branch that should never run — a programmer error, not a domain outcome — so routing them through the error channel would hand callers a Left they can neither handle nor meaningfully report. Set ignore_unimplemented: false to report them too.

Only fpdart’s own combinators are checked. A throw inside Iterable.map is ordinary Dart and is never reported.

analysis_options.yaml
many_lints:
rules:
avoid_throw_in_fp_callback:
ignore_unimplemented: false
additional_methods:
- traverse
Option Type Default Description
ignore_unimplemented bool true Allow thrown UnimplementedError, UnsupportedError, StateError and AssertionError, which mark unreachable branches
methods list of strings the seven chaining methods Replace the set of callback-taking methods that are checked
additional_methods list of strings [] Extend the set, for projects that wrap fpdart’s combinators in their own

This rule is in the core preset, so it is on with preset: core, preset: recommended or preset: opinionated.

To turn it off:

many_lints.yaml
rules:
avoid_throw_in_fp_callback: false

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