In the bloc package, what does addError do on a Cubit, and how does it differ from an exception thrown inside a Bloc event handler?
answer
- report, do not throw
- onError then the observer
- state stays unchanged
- handler errors also reach onDone
- a plain throw in a Cubit is invisible
basics
~20 saddError reports a caught error through the instance's onError to BlocObserver.onError without changing state or throwing. A Bloc handler that throws is reported the same way, then onDone receives the error and the exception is rethrown as an uncaught async error.
solid answer
~40 s`addError(error, [stackTrace])` is a `@protected` method on every `Bloc` and `Cubit`: it calls the instance's `onError` (defaulting the stack trace to `StackTrace.current`), which forwards to `BlocObserver.onError`. It does not emit a state, does not touch `stream` and does not throw, so a Cubit typically calls it in a `catch` block next to `emit(failureState)`. An exception thrown inside a Bloc's `on<E>` handler is caught by the bloc, reported through `onError`, passed to `onDone` along with the stack trace, and then rethrown - it surfaces as an uncaught asynchronous error for the app's global error handling, while the bloc keeps processing later events. An exception thrown from an ordinary Cubit method, by contrast, is just a Dart exception: the observer never sees it unless you call `addError`.
go deeper
Remember that addError reports an error to onError and the BlocObserver, but does not change state or throw.
Contrast the three paths: addError, a throw in a Bloc handler with its onDone, and a throw in a Cubit method that nobody reports.
Design handlers so expected failures become states plus addError, while unexpected ones still reach global error handling with their original stack traces.
Set a team convention for which failures become states, which are only reported, and how observer-level reporting avoids duplicates with global handlers.
## The error path in the bloc package Every `Bloc` and `Cubit` inherits two error members from `BlocBase`: - **`onError(error, stackTrace)`** - a `@protected`, `@mustCallSuper` hook whose base implementation forwards to `BlocObserver.onError`. Override it to add per-instance handling, and call `super.onError` last. - **`addError(error, [stackTrace])`** - a `@protected` method that simply calls `onError(error, stackTrace ?? StackTrace.current)`. So `addError` is a way to **report**, not to **fail**. Nothing is emitted, the state stream sees no error event, and the caller is not interrupted. ## Using addError in a Cubit A reading-progress cubit that loads the last position from a repository might do: ```dart Future<void> load(String bookId) async { try { final progress = await _repository.fetch(bookId); emit(ReadingProgressState.ready(progress)); } catch (error, stackTrace) { addError(error, stackTrace); emit(const ReadingProgressState.unavailable()); } } ``` The UI learns about the failure from the **state**; the observer learns about it from **onError**. Passing the caught `stackTrace` keeps the original trace instead of one pointing at the `catch` block. ## What happens to errors you do not report The bloc package only sees errors that go through `onError`: 1. **A throw inside a Cubit method.** A Cubit method is plain Dart. If it throws, the exception propagates to the caller (often a widget callback) and the observer is never told. 2. **A throw inside a Bloc `on<E>` handler.** The bloc wraps each handler call. On an exception it calls `onError`, then `onDone(event, error, stackTrace)`, and then rethrows. Because the handler's future is not awaited by anyone, the rethrow becomes an uncaught asynchronous error, which the app's global error hooks receive. Later events are still handled. 3. **`emit` on a closed instance.** `emit` throws a `StateError` ('Cannot emit new states after calling close'), reporting it through `onError` before rethrowing. | Source | Reaches `BlocObserver.onError`? | Also | Thrown to the caller? | |---|---|---|---| | `addError` | yes | nothing else | no | | throw in a Bloc handler | yes | `onDone` gets the error | not to `add`'s caller - rethrown as an uncaught async error | | throw in a Cubit method | no | - | yes | | `emit` after `close()` | yes | - | yes, `StateError` | ## Choosing between them - Catch expected failures (network, parsing) in the handler or cubit method, emit a failure state, and `addError` so they are logged. - Let genuinely unexpected exceptions in a Bloc handler throw: they are reported and reach the global handlers, which is what you want for bugs. - In a Cubit, remember that nothing reports a throw for you. ## Common mistakes - Calling `addError` and expecting the UI to react: the state did not change, so no widget rebuilds. - Swallowing an exception with an empty `catch` and neither emitting nor reporting. - Overriding `onError` and forgetting `super.onError`, which hides the error from the observer.
- Does calling addError put an error on the Cubit's stream so a StreamBuilder sees it?No. `addError` only calls `onError`, which forwards to `BlocObserver.onError`. The state stream receives nothing, so widgets need a failure state emitted alongside it if they should react.
- After a Bloc handler throws, can the bloc still handle new events?Yes. The bloc reports the error, calls `onDone` with it, and rethrows it from that one handler invocation. Its event subscription stays alive, so later events are delivered to their handlers as usual.
saying these in an interview costs you the question
- Believes addError emits an error on the state stream for widgets to see.
- Assumes a Cubit reports every exception its methods throw to the observer.
- Thinks one throwing Bloc handler permanently stops the bloc from handling events.
- Calls addError without passing the caught stack trace.
- Overrides onError and drops the call to super.onError.