Skip to content

fix(bloc): make isClosed return true immediately when close is called#4759

Open
ersanKolay wants to merge 2 commits intofelangel:masterfrom
ersanKolay:fix/bloc-is-closed-race-condition
Open

fix(bloc): make isClosed return true immediately when close is called#4759
ersanKolay wants to merge 2 commits intofelangel:masterfrom
ersanKolay:fix/bloc-is-closed-race-condition

Conversation

@ersanKolay
Copy link
Copy Markdown
Contributor

Summary

  • Fixes a race condition where isClosed returned false during Bloc.close() teardown, but add() would throw a StateError because the internal _eventController was already closed.
  • Root cause: isClosed was based on _stateController.isClosed, but Bloc.close() closes _eventController first. This created a window where checking isClosed before add() was unreliable.
  • Introduces a _closed flag in BlocBase set at the start of close(), and overrides isClosed in Bloc to also check _eventController.isClosed.
  • Internal emit/onEmit checks use _stateController.isClosed directly so in-progress event handlers can still emit states during teardown.

Closes #4749

Test plan

  • All 118 existing tests pass
  • New test: Bloc.isClosed returns true immediately when close() is called (without await)
  • New test: Cubit.isClosed returns true immediately when close() is called (without await)
  • dart analyze clean (no errors or warnings)

Previously isClosed was based on _stateController.isClosed, which only
became true after the full async teardown completed. In Bloc, the
_eventController closes first during close(), creating a window where
isClosed returned false but add() would throw a StateError.

This introduces a _closed flag set at the start of close(), and
overrides isClosed in Bloc to also check _eventController.isClosed.
Internal emit/onEmit checks use _stateController.isClosed directly
so in-progress handlers can still emit during teardown.

Closes felangel#4749
@ersanKolay ersanKolay requested a review from felangel as a code owner March 11, 2026 13:37
@felangel felangel closed this Mar 11, 2026
@felangel felangel reopened this Mar 11, 2026
@felangel
Copy link
Copy Markdown
Owner

Thanks for the PR but I’m not sure this is the correct fix. I think the first step here is to make a minimal reproduction sample. The tests you added don’t reflect the issue linked.

Adds a test that reproduces the exact scenario from felangel#4749:
a Bloc using emit.onEach where the onData callback guards add()
with isClosed, but external stream emits during close() teardown
causing a StateError because _eventController closes before
_stateController.
@ersanKolay
Copy link
Copy Markdown
Contributor Author

Thanks for the feedback. I added a minimal reproduction sample as a comment on #4749 and updated the PR with a test that reproduces the exact scenario from the issue: a Bloc using emit.onEach where onData guards add() with isClosed, but external stream data during close() teardown causes a StateError.

Let me know if you'd like a different approach for the fix itself.

@felangel
Copy link
Copy Markdown
Owner

Thanks for the feedback. I added a minimal reproduction sample as a comment on #4749 and updated the PR with a test that reproduces the exact scenario from the issue: a Bloc using emit.onEach where onData guards add() with isClosed, but external stream data during close() teardown causes a StateError.

Let me know if you'd like a different approach for the fix itself.

Thanks I'll take a look shortly! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

isClosed is false although the bloc is closed

2 participants