Skip to content

async_value_nullable_pattern

v0.8.0 Warning Fix Riverpod State

Flags AsyncValue(:final value?) when the value type is nullable. The ? pattern matches only when the value is non-null, which is not the same question as whether a value has loaded.

For an AsyncValue<int?>, a successfully loaded null is a real result. The ? pattern rejects it, so that case silently falls through to the loading or error branch — the UI shows a spinner forever for data that actually arrived. hasValue: true asks the question you meant: has this loaded?

See also: Riverpod - AsyncValue

A profile screen whose provider yields User? — null meaning “signed out”, which is a perfectly good loaded value. The ? pattern rejects it, so a signed-out user gets a spinner that never stops:

final currentUserProvider = FutureProvider<User?>((ref) => auth.currentUser());
class ProfileView extends ConsumerWidget {
const ProfileView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(currentUserProvider);
return switch (user) {
AsyncValue(:final value?) => Text(value.name), // LINT
AsyncValue(:final error?) => Text('$error'),
_ => const CircularProgressIndicator(),
};
}
}

Ask the question you meant — has this loaded? — and handle the null yourself:

final currentUserProvider = FutureProvider<User?>((ref) => auth.currentUser());
class ProfileView extends ConsumerWidget {
const ProfileView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(currentUserProvider);
return switch (user) {
AsyncValue(:final value, hasValue: true) =>
Text(value?.name ?? 'Signed out'),
AsyncValue(:final error?) => Text('$error'),
_ => const CircularProgressIndicator(),
};
}
}

The rule stays silent where the ? pattern is already precise.

A non-nullable value type — null can then only mean “not loaded”, which is exactly what ? asks:

void fn(AsyncValue<int> asyncValue) {
switch (asyncValue) {
case AsyncValue(:final value?):
print(value);
default:
break;
}
}

Matching AsyncData rather than AsyncValue — AsyncData.hasValue is always true, so the null check is doing real work:

void onData(AsyncValue<int?> asyncValue) {
switch (asyncValue) {
case AsyncData(:final value?):
print(value);
default:
break;
}
}

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

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