Skip to content

check_is_not_closed_after_async_gap

v0.8.0WarningConfigurableAsync Safety

This rule flags an emit(...) call that happens after an await inside a Bloc or Cubit, without an intervening if (isClosed) return; guard.

A bloc can be closed while an asynchronous handler is suspended — the user navigates away mid-request, or the provider holding the bloc is disposed. When the awaited future finally completes, the handler resumes and emits into a bloc that no longer exists, which throws a StateError.

The failure is easy to miss: the throw happens inside a detached future, so it usually surfaces as an unhandled async error in the console rather than a crash you can trace. In tests it often does not appear at all.

Guarding with if (isClosed) return; after each await makes the handler exit cleanly instead.

This is the bloc counterpart to use_ref_and_state_synchronously and to Dart’s own use_build_context_synchronously.

See also: bloc: BlocBase.isClosed

class UserCubit extends Cubit<UserState> {
UserCubit() : super(UserState.initial());
Future<void> load() async {
final user = await repository.fetchUser();
// The cubit may have been closed while this was suspended
emit(UserState.loaded(user));
}
}
class UserCubit extends Cubit<UserState> {
UserCubit() : super(UserState.initial());
Future<void> load() async {
final user = await repository.fetchUser();
if (isClosed) return;
emit(UserState.loaded(user));
}
}

A guard resets the tracking, so a second await after it needs its own guard:

Future<void> loadTwice() async {
final a = await repository.fetchA();
if (isClosed) return;
emit(UserState.loaded(a));
final b = await repository.fetchB();
if (isClosed) return; // needed again
emit(UserState.loaded(b));
}

Both guard shapes are recognised: the early return if (isClosed) return; and the inverted wrapper if (!isClosed) { emit(...); }.

Statements are scanned in source order within one function body. An emit inside a nested closure is not reported, because that closure runs on its own schedule and its guards cannot be reasoned about from the enclosing scope. A guard hidden behind a helper method — if (_shouldStop()) return; — is also not recognised.

This rule is in the opinionated preset, so it is on with preset: opinionated, or by name:

many_lints.yaml
rules:
check_is_not_closed_after_async_gap: true

To turn it off again:

many_lints.yaml
rules:
check_is_not_closed_after_async_gap: false

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

analysis_options.yaml
many_lints:
rules:
check_is_not_closed_after_async_gap:
additional_methods: [safeEmit]
Option Type Default Description
additional_methods list of strings [] Extra methods treated like emit, for a project wrapper that forwards to it