Skip to content

avoid_future_of_either

v1.0.0WarningConfigurablefpdart

This rule flags a function or method returning Future<Either<L, R>>, which TaskEither<L, R> already expresses.

Future<Either<L, R>> is not a bug — unlike Either<L, Future<R>>, which avoid_either_of_future reports. It is simply the same thing TaskEither is, with the composition thrown away.

A caller cannot flatMap a Future<Either> without awaiting first, so every chain has to leave the fpdart world and come back — see the example below.

The eagerness matters too. A Future starts running the moment it is created, so a Future<Either> cannot be retried, delayed, or built up now and run later. A TaskEither describes the work instead of performing it.

class Failure {
const Failure(this.message);
final String message;
}
class User {
const User(this.id);
final String id;
}
Future<User> getUserRaw(String id) async => User(id);
Future<Either<Failure, User>> getUser(String id) async {
try {
return right(await getUserRaw(id));
} catch (error) {
return left(Failure('$error'));
}
}

A caller cannot flatMap that without awaiting first, so every chain has to leave the fpdart world and come back:

final either = await getUser(id);
final result = await either.match(
(failure) async => left<Failure, List<Order>>(failure),
(user) => loadOrders(user.id),
);
TaskEither<Failure, User> getUser(String id) => TaskEither.tryCatch(
() => getUserRaw(id),
(error, stackTrace) => Failure('$error'),
);

The same pipeline is now one expression, because the type carries both the asynchrony and the failure channel:

final result = await getUser(id).flatMap((user) => loadOrders(user.id)).run();

FutureOr<Either<L, R>> is not reported: it may complete synchronously, so it is a different shape with a different answer.

A generator (Stream<Either<L, R>> with async*) is never reported — one TaskEither cannot stand in for a stream of values.

The reverse nesting, Either<L, Future<R>>, belongs to avoid_either_of_future; the two rules never report the same line.

Future<Option<T>> is the same argument for the other wrapper and has its own rule, avoid_future_of_option, so a project can enable either half alone.

No quick fix is offered: the conversion changes a public signature, so every call site needs updating and a “apply all” would leave the project uncompilable.

There is an assist. Put the cursor on the function and pick “Convert to TaskEither” — it rewrites the signature and moves the body into a TaskEither. See Assists for what it does with call sites.

analysis_options.yaml
many_lints:
rules:
avoid_future_of_either:
ignore_private: true
Option Type Default Description
ignore_private bool false Skip private functions and methods

The default is false because a Future<Either> is awkward to consume wherever it appears — unlike a try/catch, which really is an implementation detail.

This rule is in the opinionated preset. With a lower preset, enable it by name with avoid_future_of_either: true.

To turn it off:

many_lints.yaml
rules:
avoid_future_of_either: false

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