Skip to content

prefer_unit_over_void

v1.0.0WarningFixConfigurablefpdart

This rule flags an fpdart type parameterised with void — TaskEither<Failure, void>, Option<void>, and so on.

void is not a value in Dart. You cannot pass it, store it, or hand it to the next step of a pipeline. A TaskEither<Failure, void> therefore stops composing: flatMap on it has nothing meaningful to bind, and callers end up unwrapping to null and re-deriving “did it succeed” from that — which is exactly the manual result-handling fpdart exists to delete.

Unit is fpdart’s answer. It is a real type with exactly one value, so it says “succeeded, with nothing to report” while staying a value the pipeline can carry.

See also: fpdart: Unit

void is not a value, so this signature has nothing for the next step to bind — flatMap on it is a dead end:

class Failure {
const Failure(this.message);
final String message;
}
class User {
const User(this.id);
final String id;
}
class UserRepository {
TaskEither<Failure, void> save(User user) => TaskEither.of(null);
}

Callers end up unwrapping to null and re-deriving “did it succeed” from that — the manual result handling fpdart exists to delete.

Unit is a real type with exactly one value, so the pipeline can carry it:

class UserRepository {
TaskEither<Failure, Unit> save(User user) => TaskEither.of(unit);
}

The value to return is the unit constant:

class UserRepository {
TaskEither<Failure, Unit> save(User user) => TaskEither.tryCatch(() async {
await _write(user);
return unit;
}, (error, stackTrace) => Failure('$error'));
Future<void> _write(User user) async {}
}

And the chain now composes, because save hands the next step a value:

TaskEither<Failure, String> saveAndConfirm(UserRepository repo, User user) =>
repo.save(user).map((_) => 'saved ${user.id}');

A quick fix replaces the void type argument with Unit and adds the package:fpdart/fpdart.dart import if it is missing. It can be applied across a whole file at once.

The fix deliberately stops there. Once the type says Unit, a body that returned nothing no longer compiles, and you have to write return unit; — the fix will not guess where in an arbitrary body that belongs. A fix that rewrites control flow to satisfy a type annotation is not one anybody should apply without reading it.

A member marked @override is not reported by default: its signature is fixed by the supertype, so the change belongs there instead. Set ignore_overrides: false to report those too.

analysis_options.yaml
many_lints:
rules:
prefer_unit_over_void:
ignore_overrides: false
Option Type Default Description
ignore_overrides bool true Skip members marked @override, whose signature is fixed by the supertype

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

To turn it off:

many_lints.yaml
rules:
prefer_unit_over_void: false

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