Exceptions handling with safe_bloc
Exceptions handling with safe_bloc
Exceptions handling with safe_bloc

Exceptions handling with safe_bloc

Flutter

Flutter

Flutter

Error handling in Flutter can sometimes be a daunting task. How does the safe_bloc library help to solve this problem?

11. 10. 2023

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.

abstract class SafeBloc<EVENT, STATE> extends Bloc<EVENT, STATE> with BlocPresentationMixin<STATE, BaseEffect> {
  SafeBloc(super.initialState);
}

Following that, we've introduced a new method called onSafe<EVENT>.

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.

void onSafe<E extends EVENT>(
    EventHandler<E, STATE> handler, {
    String? message,
    bool isAction = false,
    bool ignoreError = false,
    EventTransformer<E>? transformer,
  }) {
    on(
      (event, emit) {
        try {
          handler(event, emit);
        } catch (e, stacktrace) {
          debugPrint(message ?? 'Unexpected error occurred in cubit$runtimeType,$stacktrace');

          if (ignoreError) return;

          final error = UnexpectedError(
            error: e,
            stackTrace: stacktrace,
            message: message,
            isAction: isAction,
          );

          if (isAction) {
            emitPresentation(UnexpectedErrorEffect(error));
          } else {
            emit(errorState(error));
          }
        }
      },
      transformer: transformer,
    );
  }

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 SafeBloc’s method:

@protected
STATE Function(UnexpectedError error) get errorState;

Both error state and error effect must implement UnexpectedErrorBaseBase and override its error getter.

abstract class UnexpectedErrorBase {
  UnexpectedError get error;

  const UnexpectedErrorBase();
}

This getter returns the UnexpectedError object that contains useful information about the exception – stack trace, passed message and 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.

class UnexpectedErrorHandler<BLOC extends SafeBlocBase<STATE, BaseEffect>, STATE> extends StatelessWidget {
   final Widget Function(BuildContext context, UnexpectedError error) errorScreen;
   final Future<void> Function(BuildContext context, UnexpectedError error) onErrorAction;
   final Widget child;

  const UnexpectedErrorHandler({
    required this.errorScreen,
    required this.onErrorAction,
    required this.child,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final bloc = context.watch<BLOC?>();

    if (bloc == null) return child;

    return BlocPresentationListener<BLOC, BaseEffect>(
      listener: (context, effect) {
        if (effect case final UnexpectedErrorBase error) {
          onErrorAction(context, error.error);
        }
      },
      child: BlocBuilder<BLOC, STATE>(
        builder: (context, state) {
          if (state is UnexpectedErrorBase) {
            return Builder(
              builder: (context) {
                return errorScreen(context, state.error);
              },
            );
          }

          return child;
        },
      ),
    );
  }
}

Using this widget in combination with SafeBloc, we created a nice mechanism to inform the user whenever an exception occurs in the app.

Summary

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.

Further readings

safe_bloc on GitHub

safe_bloc on pub.dev

Sources

Bloc - Core Concepts - Error Handling

Flutter Exception Handling with try/catch and the Result type

Podobné články

Podobné články

Podobné články