Skip to content

avoid_unrun_task

v1.0.0WarningConfigurablefpdart

This rule flags a Task, TaskEither, IO, IOEither, TaskOption or IOOption that is evaluated as a statement and then discarded, without .run() ever being called on it.

fpdart’s lazy types are descriptions of work, not the work itself. TaskEither.tryCatch(...) builds a plan; nothing executes until .run(). Dropping the value therefore does not merely waste a result — it skips the operation entirely. No request is sent, no row is written, no exception is thrown.

That silence is what makes this worse than a discarded Future, which at least ran. Here the program compiles, the types check, the test that mocks the repository still passes, and in production the save simply never happens.

This is the fpdart counterpart to the SDK’s unawaited_futures, and the one mistake in this family that the type system cannot catch on its own.

Either and Option are deliberately not reported: they are already-computed values, so discarding one wastes a result but never skips an effect.

See also: fpdart: Task, unawaited_futures

save returns a TaskEither, which is a description of the write. Dropping it means no row is ever written — and nothing says so:

class Failure {
const Failure(this.message);
final String message;
}
class User {
const User(this.id);
final String id;
}
class UserRepository {
TaskEither<Failure, Unit> save(User user) => TaskEither.of(unit);
}
void persist(UserRepository repository, User user) {
repository.save(user); // never runs
}

Run it, and await the result:

Future<void> persist(UserRepository repository, User user) async {
await repository.save(user).run();
}

Or return the pipeline and let the caller own running it — often the better answer, since the caller is the one that can report the failure:

TaskEither<Failure, Unit> persist(UserRepository repository, User user) =>
repository.save(user);

The same mistake hides mid-pipeline, where a step is built and then not joined on:

Future<void> checkout(UserRepository repository, User user) async {
repository.save(user).map((_) => 'saved'); // still never runs
await Future<void>.delayed(Duration.zero);
}

Chain it into the pipeline that is run:

Future<void> checkout(UserRepository repository, User user) async {
await repository.save(user).map((_) => 'saved').run();
}

Only a discarded statement is reported. A value assigned to a variable is assumed to be run later, since the rule does not track what happens to it afterwards.

analysis_options.yaml
many_lints:
rules:
avoid_unrun_task:
additional_types:
- LazyOperation
ignore_cascades: true
Option Type Default Description
additional_types list of strings [] Extra type names to treat as lazy, for projects that wrap fpdart’s types in their own
ignore_cascades bool false Skip a lazy value discarded as the target of a cascade

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_unrun_task: false

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