Error handling is an issue that every developer has to face at some stage of any app development, and Flutter is no exception. In this article, we focus on exceptions in apps that use blocs for state management. These errors can have different origins, they can be errors coming from external libraries, API failures, or developer's mistakes in code. Either way, we should always be prepared that an exception can occur and handle it properly to provide a good user experience. Considering that, we should include a try-catch block in every bloc’s event handler to manage potential exceptions. However, this approach can become tedious and result in repetitive code. In this article, we will present safe_bloc library that provides a global solution to this problem. This library extends blocs and cubits to handle the exceptions in a general way, including displaying error messages to the user and logging errors for debugging purposes.
What happens if exceptions are ignored in a bloc
If an unhandled exception occurs in any bloc method, the widget tree usually remains unaffected so that the user should not see anything bad happen directly. Unfortunately, in most cases thrown exception means that something got really wrong and the UI action was probably interrupted in the middle and not completed properly. As follows, the user will probably encounter some non-standard behavior of the app without warning (app freezes, no buttons work, etc.), which can lead to very poor user experience.
Extending Bloc with error handling BlocBase
In order to prevent the app from this unwanted behavior, we want to notify the user that something went wrong.
Bloc library documentation suggests overriding the
onError method in order to handle exceptions. This is perfect for logging the exception, however, it isn’t sufficient for our purpose. It doesn’t have an emitter in its parameters so any state cannot be emitted, and there is no way to inform the UI that the exception was thrown. Additionally, bloc internally rethrows the exception after calling the onError method thus it is still propagates through the bloc. Also, it cannot be parameterized, and we don’t know from which event handler it comes from.
To handle errors in a more efficient way, we distinguish 2 types of bloc events:
Initial data loading – This event is triggered only when the screen is first opened and needs to fetch its initial data. If the screen loading fails, there's no data to display to the user, and an error screen should be displayed instead.
User actions – These events typically happen when a user interacts with an already loaded screen, typically by taking actions like pressing a button. In this scenario, we don't want to disrupt the user's experience by displaying an error screen and erasing the loaded data. Instead, it’s better to display an error dialog or a snackbar, informing the user that the action is currently unavailable. This ensures that the screen's existing data remains accessible to the user.
To address these errors, we created an abstract class
SafeBloc that extends a
Bloc class and uses a
BlocPresentationMixin (taken from the package bloc_presentation). This mixin adds an additional stream of events (so-called effects) to the bloc and is useful for managing one-time events, such as displaying an error dialog. You can find more details about handling one-time events in our other article, How to handle one-time UI events with Bloc in Flutter.
Following that, we've introduced a new method called
This method wraps the existing bloc's
on<EVENT> method and contains a try-catch block to handle exceptions in the event
handler. Additionally, this method allows you to pass further parameters, such as a message that can be logged or a boolean
ignoreError , which determines whether to skip the error handling process.
Moreover, there is another helpful parameter
isAction that specifies whether the event is a user action. In detail it means that if it’s set to true and exception occurs, an error dialog will pop up to inform the user about the error. If set to false, the entire screen will be replaced with the error widget. This is particularly useful in scenarios like initial data loading or when you want to prevent the user from taking any further action.
Each time an exception occurs, it’s caught and an error state is emitted in case of initial data loading, or a side effect is produced in case of user action. The error state is provided to the bloc by overriding
Both error state and error effect must implement
UnexpectedErrorBaseBase and override its error getter.
This getter returns the
UnexpectedError object that contains useful information about the exception – stack trace, passed
isAction boolean from
onSafe<EVENT> method and the exception itself. These information propagates with the error state/effect to the UI where can be used.
Displaying unexpected errors to user
To reflect the error state and error effect in UI we created a widget named UnexpectedErrorHandler that wraps every screen in our app. It listens to produced error effects and displays error dialogs accordingly. Also, it employs a BlocBuilder that rebuilds the screen to an error screen each time an error state is emitted.
Using this widget in combination with SafeBloc, we created a nice mechanism to inform the user whenever an exception occurs in the app.
This is a simple example of unified error handling in the bloc. The same approach is employed in the safe_bloc library for cubits, so it gives you the flexibility to choose whichever approach best fits your needs.
Try it yourself and let us know on our Discord how you liked it or if you have ideas for improvement.