check-is-not-closed-after-async-gap

Path
lint/correctness/check-is-not-closed-after-async-gap
Default
On (recommended)
Origin
Falcon
Domain
bloc
Rule
201 of 512
recommendedbloc

Flags Bloc add and emit calls after an await without a fresh isClosed check.

A Bloc or Cubit can close while asynchronous work is suspended. Check !isClosed before dispatching or emitting, or return when isClosed is true. Each await invalidates earlier checks. Both guarded branches and terminating early-return guards are recognized, and nested functions are independent. Awaits in call arguments invalidate even an enclosing guard. Calls must target the enclosing package-backed Bloc/Cubit, or an Emitter parameter of its event handler. An Emitter also accepts its own !isDone guard. Unrelated receivers, shadowed names and opaque mixins are ignored. Analysis follows straight-line statements and enclosing if branches; uncertain loops, switch/try flow, pattern conditions, conditional collection elements and optional/short-circuit evaluation are not treated as proof of an unguarded asynchronous path.

Bad

await load();
emit(Loaded());

Good

await load();
if (isClosed) return;
emit(Loaded());

Invalid

example.dartdart
import 'package:bloc/bloc.dart';

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  Future<void> unguarded() async {
    await load();
    emit(1);
  }
  Future<void> explicitReceiver() async {
    await load();
    this.emit(2);
  }
  Future<void> staleCheck() async {
    if (!isClosed) {
      await load();
      emit(3);
    }
  }
  Future<void> wrongGuard(CounterCubit other) async {
    await load();
    if (!other.isClosed) {
      emit(4);
    }
  }
  Future<void> staleReturn() async {
    if (isClosed) return;
    await load();
    emit(5);
  }
  Future<void> awaitedArgument() async {
    emit(await value());
  }
  Future<void> guardedAwaitedArgument() async {
    if (!isClosed) emit(await value());
  }
  Future<void> gapBeforeConditionalArgument(bool ready) async {
    await load();
    emit(ready ? await value() : 0);
  }
  Future<int> value() async => 6;
  Future<void> load() async {}
}

class CounterBloc extends Bloc<int, int> {
  CounterBloc() : super(0) {
    on<int>((event, emit) async {
      await load();
      add(1);
      emit(2);
    });
  }
  Future<void> handle(int event, Emitter<int> emit) async {
    await load();
    emit(3);
  }
  Future<void> shadowedGuard(bool isClosed) async {
    await load();
    if (!isClosed) {
      add(4);
    }
  }
  Future<void> load() async {}
}
Check `isClosed` after the `await` before adding an event or emitting a state.
6 await load();
7 emit(1);
Check `isClosed` after the `await` before adding an event or emitting a state.
10 await load();
11 this.emit(2);
Check `isClosed` after the `await` before adding an event or emitting a state.
15 await load();
16 emit(3);
Check `isClosed` after the `await` before adding an event or emitting a state.
21 if (!other.isClosed) {
22 emit(4);
Check `isClosed` after the `await` before adding an event or emitting a state.
27 await load();
28 emit(5);
Check `isClosed` after the `await` before adding an event or emitting a state.
30 Future<void> awaitedArgument() async {
31 emit(await value());
Check `isClosed` after the `await` before adding an event or emitting a state.
33 Future<void> guardedAwaitedArgument() async {
34 if (!isClosed) emit(await value());
Check `isClosed` after the `await` before adding an event or emitting a state.
37 await load();
38 emit(ready ? await value() : 0);
Check `isClosed` after the `await` before adding an event or emitting a state.
47 await load();
48 add(1);
Check `isClosed` after the `await` before adding an event or emitting a state.
48 add(1);
49 emit(2);
Check `isClosed` after the `await` before adding an event or emitting a state.
53 await load();
54 emit(3);
Check `isClosed` after the `await` before adding an event or emitting a state.
58 if (!isClosed) {
59 add(4);

Valid

example.dartdart
import 'package:bloc/bloc.dart';

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void synchronous() { emit(0); }
  Future<void> branchGuard() async {
    await load();
    if (!isClosed) { emit(1); }
  }
  Future<void> conjunctionGuard(bool ready) async {
    await load();
    if (!isClosed && ready) { emit(1); }
  }
  Future<void> disjunctionReturn(bool paused) async {
    await load();
    if (isClosed || paused) return;
    emit(1);
  }
  Future<void> returnGuard() async {
    await load();
    if (isClosed) return;
    emit(2);
  }
  Future<void> elseGuard() async {
    await load();
    if (this.isClosed) { print('closed'); } else { this.emit(3); }
  }
  Future<void> throwGuard() async {
    await load();
    if (isClosed) { throw StateError('closed'); }
    emit(4);
  }
  Future<void> freshGuard() async {
    await load();
    if (isClosed) return;
    await load();
    if (isClosed) return;
    emit(5);
  }
  Future<void> nestedIsolation() async {
    await load();
    final callback = () { emit(6); };
    print(callback);
  }
  Future<void> nestedAwaitIsolation() async {
    final callback = () async { await load(); };
    print(callback);
    emit(7);
  }
  void nestedAwaitArgument() {
    emit((() async { await load(); return 7; }).hashCode);
  }
  Future<void> conditionalAwaitArgument(bool ready) async {
    emit(ready ? await value() : 0);
  }
  Future<int> value() async => 7;
  Future<void> unrelatedReceiver(CounterCubit other) async {
    await load();
    other.emit(8);
  }
  Future<void> localShadow(void Function(int) emit) async {
    await load();
    emit(9);
  }
  Future<void> shortCircuit() async {
    await load();
    !isClosed && send();
  }
  bool send() { emit(10); return true; }
  Future<void> load() async {}
}

class Service {
  Future<void> run() async {
    await load();
    emit(1);
  }
  void emit(int value) {}
  Future<void> load() async {}
}

class CustomCubit extends Cubit<int> {
  CustomCubit() : super(0);
  void emit(int value) {}
}

class ChildCustomCubit extends CustomCubit {
  Future<void> inheritedOverride() async {
    await Future<void>.value();
    emit(1);
  }
}

Configure check-is-not-closed-after-async-gap

Set its severity in falcon.json. See the configuration guide.

falcon.jsonjson
{
  "linter": {
    "rules": {
      "correctness": {
        "check-is-not-closed-after-async-gap": "error"
      }
    }
  }
}

Or silence one occurrence with a suppression comment; the reason is required.

example.dartdart
// falcon-ignore lint/correctness/check-is-not-closed-after-async-gap: <reason>