BLoC Global Error Handler: Centralized Error Management with BlocObserver
Last Sync: Today
flutter
BLoC Global Error Handler: Centralized Error Management with BlocObserver
What is Global Error Handling in BLoC?
Global error handling in BLoC refers to a centralized mechanism that catches and processes errors occurring in any BLoC or Cubit within your application. Instead of handling errors individually inside each BLoC’s event handlers, you can leverage BlocObserver to intercept all errors, log them, show user notifications, and optionally trigger custom recovery logic. This approach ensures consistent error handling, reduces boilerplate, and improves maintainability.
Why Use a Global Error Handler?
Consistency – All errors are handled uniformly (logging, analytics, UI feedback).
Reduced Boilerplate – No need to wrap every async operation in try/catch inside each BLoC.
Centralized Logging – Easier to monitor and debug issues in production.
Better UX – Show user-friendly messages without scattering Snackbars or dialogs across the app.
Separation of Concerns – BLoCs focus on business logic; error handling is delegated to a dedicated layer.
Setting Up a Custom BlocObserver
BlocObserver is an abstract class provided by flutter_bloc that allows you to observe changes in BLoCs and Cubits. Override the onError method to catch all errors that occur inside BLoCs or Cubits.
DARTRead-only
1
import'package:flutter_bloc/flutter_bloc.dart';classAppBlocObserverextendsBlocObserver{
@override
voidonChange(BlocBase bloc, Change change){super.onChange(bloc, change);// Optionally log state changes}
@override
voidonError(BlocBase bloc, Object error, StackTrace stackTrace){super.onError(bloc, error, stackTrace);// Centralized error handlingprint('Error in $bloc: $error');// Send to analytics, crash reporting, or show a snackbar// You can also dispatch a global error event if needed}}
Registering the BlocObserver
You must set the Bloc.observer before running the app. This is typically done in main().
Errors thrown inside event handlers will automatically be caught by the observer, but the BLoC’s state won’t change. You need to decide whether to emit an error state or let the global handler show a notification. A common pattern is to emit an error state and also rely on the observer for logging.
DARTRead-only
1
classLoginBlocextendsBloc<LoginEvent, LoginState>{
final AuthRepository authRepository;LoginBloc({required this.authRepository}):super(LoginInitial()){
on<LoginSubmitted>((event, emit) async {emit(LoginLoading());try{
final user =await authRepository.login(event.email, event.password);emit(LoginSuccess(user));}catch(error, stackTrace){// The observer will also catch this error, but we emit an error stateemit(LoginFailure(error.toString()));// If you rethrow, the observer will catch it again – not necessary}});}}
Global Error State Management
Instead of handling UI feedback inside the observer (which lacks BuildContext), you can create a dedicated global error bloc or use a callback system. For example, use a StreamController or a global ScaffoldMessenger key.
DARTRead-only
1
// Global messenger key
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =GlobalKey();classAppBlocObserverextendsBlocObserver{
@override
voidonError(BlocBase bloc, Object error, StackTrace stackTrace){super.onError(bloc, error, stackTrace);// Show snackbar using the global key
scaffoldMessengerKey.currentState?.showSnackBar(SnackBar(content:Text('An error occurred: $error')),);// Log to crashlytics// Crashlytics.instance.recordError(error, stackTrace);}}// In MaterialAppMaterialApp(scaffoldMessengerKey: scaffoldMessengerKey,...)
Combining with Result/Either Pattern
For more granular error handling, combine global error handling with a Result or Either type from packages like dartz or fpdart. This allows you to handle errors in a functional way and still leverage the observer for unexpected crashes.
DARTRead-only
1
classLoginBlocextendsBloc<LoginEvent, LoginState>{
final AuthRepository authRepository;LoginBloc(this.authRepository):super(LoginInitial()){
on<LoginSubmitted>((event, emit) async {emit(LoginLoading());
final result =await authRepository.login(event.email, event.password);
result.fold((failure)=>emit(LoginFailure(failure.message)),(user)=>emit(LoginSuccess(user)),);});}}// The observer will still catch any unhandled exceptions from the repository
Handling Errors in Cubits
Cubits also work with BlocObserver. Errors thrown inside Cubit methods are captured in onError. However, Cubits don’t have event handlers, so you need to handle errors manually or use a wrapper.
DARTRead-only
1
classCounterCubitextendsCubit<int>{CounterCubit():super(0);voidincrement(){try{// Some operation that might throwadd(1);}catch(e){// The error will still be caught by observer, but you can handle it here tooemit(-1);// error state// rethrow to let observer know
rethrow;}}}
Logging and Crash Reporting
A global error handler is perfect for integrating with crash reporting tools like Firebase Crashlytics or Sentry.
DARTRead-only
1
import'package:firebase_crashlytics/firebase_crashlytics.dart';classAppBlocObserverextendsBlocObserver{
@override
voidonError(BlocBase bloc, Object error, StackTrace stackTrace){super.onError(bloc, error, stackTrace);// Send to Crashlytics
FirebaseCrashlytics.instance.recordError(error, stackTrace);// You can also add custom keys
FirebaseCrashlytics.instance.setCustomKey('bloc', bloc.runtimeType.toString());}}
Testing Global Error Handling
You can test that errors are properly caught and processed by your observer using blocTest and mocking the observer.
DARTRead-only
1
test('emits error state and logs error',() async {
final mockObserver =_MockBlocObserver();
Bloc.observer = mockObserver;
final bloc =LoginBloc(authRepository:MockAuthRepositoryThrows());await blocTest<LoginBloc, LoginState>('emits loading then error',build:()=> bloc,act:(bloc)=> bloc.add(LoginSubmitted(email:'test',password:'test')),expect:()=>[LoginLoading(),
isA<LoginFailure>(),],verify:(_){verify(mockObserver.onError(any, any, any)).called(1);},);});
Best Practices
Use BlocObserver for logging and reporting only – Avoid heavy UI operations inside the observer; instead, use a global messenger key or emit events to a global error bloc.
Differentiate between expected and unexpected errors – Expected errors (e.g., validation failures) can be handled locally; unexpected errors (e.g., network crashes) can be caught globally.
Provide user-friendly messages – Map technical errors to readable messages either in the BLoC or in a dedicated error handler.
Don’t rethrow inside the observer – The observer is for side effects; rethrowing can crash the app.
Combine with runZonedGuarded – For errors outside BLoC (e.g., in UI callbacks), you can also use runZonedGuarded as a fallback.
Keep the observer simple – Delegate complex error handling to other services.
Common Mistakes
❌ Using BuildContext inside onError – The observer runs without a context; accessing context will fail. Use a global messenger key or a stream.
❌ Forgetting to set Bloc.observer – Without setting, errors won’t be caught centrally.
❌ Swallowing errors in BLoC without emitting an error state – The observer will catch the error, but the UI may hang in a loading state.
❌ Overriding onError without calling super.onError – This may break internal error propagation (though flutter_bloc doesn’t rely on super, it’s good practice).
❌ Handling expected errors globally – Not all errors should be shown to users; some are part of normal flow and should be handled locally.
Conclusion
Global error handling with BlocObserver is a powerful way to centralize error management in your BLoC architecture. It simplifies code, ensures consistent logging, and improves user experience by providing a single place to handle unexpected errors. By combining it with error states and a global UI feedback system, you can build robust, user-friendly Flutter applications.
Try it yourself
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
// ---------- Global Observer ----------
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey();
class AppBlocObserver extends BlocObserver {
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
super.onError(bloc, error, stackTrace);
// Show a snackbar using the global key
scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text('Global error: ${error.toString()}'),
backgroundColor: Colors.red,
),
);
// In a real app, you'd also log to crash reporting
print('ERROR in $bloc: $error');
}
}
// ---------- States ----------
abstract class CounterState extends Equatable {
const CounterState();
@override
List<Object?> get props => [];
}
class CounterInitial extends CounterState {}
class CounterLoading extends CounterState {}
class CounterValue extends CounterState {
final int value;
const CounterValue(this.value);
@override
List<Object?> get props => [value];
}
class CounterError extends CounterState {
final String message;
const CounterError(this.message);
@override
List<Object?> get props => [message];
}
// ---------- Events ----------
abstract class CounterEvent extends Equatable {
const CounterEvent();
@override
List<Object?> get props => [];
}
class Increment extends CounterEvent {}
class ThrowError extends CounterEvent {}
// ---------- BLoC ----------
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitial()) {
on<Increment>(_onIncrement);
on<ThrowError>(_onThrowError);
}
Future<void> _onIncrement(Increment event, Emitter<CounterState> emit) async {
emit(CounterLoading());
await Future.delayed(Duration(milliseconds: 300));
final currentState = state;
int newValue = 1;
if (currentState is CounterValue) {
newValue = currentState.value + 1;
}
emit(CounterValue(newValue));
}
Future<void> _onThrowError(ThrowError event, Emitter<CounterState> emit) async {
emit(CounterLoading());
await Future.delayed(Duration(milliseconds: 300));
// This error will be caught by the global observer
throw Exception('Simulated error from BLoC');
}
}
// ---------- Main ----------
void main() {
Bloc.observer = AppBlocObserver();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
scaffoldMessengerKey: scaffoldMessengerKey,
home: BlocProvider(
create: (_) => CounterBloc(),
child: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Global Error Handler Demo')),
body: BlocConsumer<CounterBloc, CounterState>(
listener: (context, state) {
if (state is CounterError) {
// Local error handling if needed
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Local: ${state.message}')),
);
}
},
builder: (context, state) {
if (state is CounterLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is CounterValue) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Counter: ${state.value}', style: TextStyle(fontSize: 32)),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: Text('Increment'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () => context.read<CounterBloc>().add(ThrowError()),
child: Text('Trigger Error'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
),
],
),
);
} else if (state is CounterError) {
return Center(child: Text('Error: ${state.message}'));
}
return Center(child: Text('Press increment'));
},
),
);
}
}
Test Your Knowledge
Q1
of 4
Which class do you extend to create a global error handler in BLoC?
A
BlocListener
B
BlocObserver
C
BlocProvider
D
BlocDelegate
Q2
of 4
How do you register the global observer?
A
Bloc.observer = MyObserver()
B
BlocObserver.set(MyObserver())
C
MaterialApp(observer: MyObserver())
D
It's automatic
Q3
of 4
What method of BlocObserver is overridden to catch errors?
A
onChange
B
onTransition
C
onError
D
onEvent
Q4
of 4
Why is using BuildContext directly inside onError problematic?
A
It causes memory leaks
B
onError runs without a valid context
C
It slows down the observer
D
BuildContext is not available in BLoCs
Frequently Asked Questions
Does BlocObserver catch errors from async gaps?
Yes, any error thrown inside an event handler (including inside Future callbacks) will be caught as long as it’s not caught internally. However, errors thrown in async functions that are not awaited may not be caught. Always use await or handle them properly.
Can I have multiple BlocObservers?
No, only one observer can be set globally. You can, however, create a composite observer that delegates to multiple handlers.
How do I handle errors that occur in Cubit methods?
Cubit methods are just regular functions; you need to try/catch inside them or rely on the observer if you rethrow. The observer will catch any uncaught error thrown from a Cubit method.
Can I use the observer to show a dialog?
You can, but you need a context. The easiest way is to use a global navigator key or a stream that a widget listens to. Showing dialogs directly in the observer is not recommended.
What about errors in BlocListener or BlocBuilder?
The observer only catches errors that originate from BLoC/Cubit event handlers. Errors in UI callbacks (like onPressed) are not caught. Use runZonedGuarded or try/catch for those.