Photo by visuals on Unsplash

Flutter error handling: a cleaned design to manage errors in your apps using the Decorator design pattern and BLoC as state management

Handling errors like a senior developer

Bernardo Iribarne
Published in
7 min readNov 3, 2023

--

Errors will happened, always, so we have to design how to handle them. First, we have to learn about the errors on the language we develop, how it manages the errors, what kind of errors we have to handle.

Types of errors

Let’s see the different types of error in Flutter:

  1. Asynchronous errors: when we use asynchronous calls we get asynchronous errors, so, we have to handle them and decide how to present them to the user.
  2. Exceptions: they could be some expected exceptions from our services, some exceptions from third-party libraries or components, etc. Also could be unexpected
  3. Errors: when the app crash. We have to handle them to enhance the user experience. Nobody wants to have a red screen (like blue one on Windows)
  4. Connectivity errors: when the app lost connectivity. Sometimes we need connectivity, for example, to perform a payment transaction, on that cases we have to present the connectivity error to the user and show some alternative.

We need to know all the errors and see how to present each one to the user, how to log it. how to handle it.

Present the error to the user

Once we have detected the different types of errors, we have to design how to show them to the user. Here is where I will show you a clean way to do it.

I assume you are familiar with State management and also that you are using it. For this example I will use BLoC pattern.

And then I will explain the Decorator design pattern which will allow us to design a clean way to add the error to our widgets.

BLoC for errors

I need a full article to talk about BLoC and state management, that is not the case. But let see a simple graphic that shows how the BLoC is involved in our design:

BLoC pattern

The BLoC manages our application states, it receives an Event, do something with that (it resolves presentation logic, call some service) and then returns the new State to the View. And the View always renders itself according to the State that it receives without resolving any logic.

We are going to have a BLoC, called ErrorBloc, that will be responsible to manage the error states. For this example I will create error bloc events and states to manage:

  1. Unexpected errors
  2. Service errors
  3. Connectivity errors

Let’s see the classes below:

ErrorBloc

import 'package:flutter_bloc/flutter_bloc.dart';
import '/src/presentation/error/bloc/error_bloc_event.dart';
import '/src/presentation/error/bloc/error_bloc_state.dart';


class ErrorBloc extends Bloc<ErrorBlocEvent, ErrorBlocState> {

ErrorBloc():super(ErrorBlocState()) {
on<UnexpectedErrorRequested>(_onUnexpectedErrorRequested);
on<ServiceErrorRequested>(_onServiceErrorRequested);
on<ConnectivityErrorRequested>(_onConnectivityErrorRequested);
}

Future<void> _onUnexpectedErrorRequested(
UnexpectedErrorRequested event,
Emitter<ErrorBlocState> emit,
) async {

emit(state.copyWith(
status: () => ErrorBlocStatus.unexpectedFailure,
message: () => event.message,
));
}

Future<void> _onServiceErrorRequested(
ServiceErrorRequested event,
Emitter<ErrorBlocState> emit,
) async {

emit(state.copyWith(
status: () => ErrorBlocStatus.serviceFailure,
message: () => event.message,
));
}

Future<void> _onConnectivityErrorRequested(
ConnectivityErrorRequested event,
Emitter<ErrorBlocState> emit,
) async {

emit(state.copyWith(
status: () => ErrorBlocStatus.connectivityFailure,
message: () => event.message,
));
}

}

ErrorBlocEvent


abstract class ErrorBlocEvent<T> {
const ErrorBlocEvent();
}

class UnexpectedErrorRequested<T> extends ErrorBlocEvent<T> {
final String message;
const UnexpectedErrorRequested(this.message);
}

class ConnectivityErrorRequested<T> extends ErrorBlocEvent<T> {
final String message;
const ConnectivityErrorRequested(this.message);
}

class ServiceErrorRequested<T> extends ErrorBlocEvent<T> {
final String message;
const ServiceErrorRequested(this.message);
}

ErrorBlocState

import 'package:equatable/equatable.dart';

enum ErrorBlocStatus { nothing, unexpectedFailure, connectivityFailure, serviceFailure }

extension ErrorBlocStatusX on ErrorBlocStatus {
bool get isError => this != ErrorBlocStatus.nothing;
}

class ErrorBlocState<T> extends Equatable {

final ErrorBlocStatus status;
String? message;

ErrorBlocState({
this.status = ErrorBlocStatus.nothing,
this.message="",
});

ErrorBlocState copyWith({
ErrorBlocStatus Function()? status,
String Function()? message
}) {
return ErrorBlocState<T>(
status: status != null ? status() : this.status,
message: message != null ? message() : this.message,
);
}

@override
List<Object?> get props => [
status,
message
];
}

Let’s see a graphic to get it clear:

Error Bloc design

They are very simple. The states only have a message which describes the error, but there you can put whatever you want to describe the error.

Decorator design pattern

Another pattern introduced by the Gang of four is the Decorator. This behavioral design pattern will hep us to present the errors to the user. We are going to decorate our widgets with the error information.

The idea of Decorator is to add/change behavior to our classes by extending them and not modifying them, so, did you note it? We are going to apply a SOLID principle: open-closed responsibility principle.

Let’s talk about this pattern:

Decorator design pattern

The Component and ConcreteComponent are the classes we want to enhance. Then we define the decorators which will enhance the components. We are going to have a generic Decorator and then several classes of ConcreteDecorator, one for each enhacement we want to add.

  1. Component: the super class that defines the generic behavior of our concrete components.
  2. ConcreteComponent: the class where we want to add/change behavior
  3. Decorator: this class is the base decorators class. It has the component to enhance. As you can see a Decorator is a Component, so it will be transparent for the class that has already used our concrete component.
  4. ConcreteDecorator: we are going to have one concrete decorator for each behavior we want to add/change on the concrete component. And it is the one in charge to apply the enhancement to the concrete component.

Implementing the Decorator

I know you’ve realized what will be do here, right, we are going to create one concrete decorator for each type a error state we defined before. And what are we going to decorate? yes, our widgets. So, let’s see how the implementations of the pattern looks like into our classes:

Error Decorator
  1. Component: a flutter widget.
  2. ConcreteComponent: could be our main widget (app) or just simple widgets.
  3. Decorator: our ErrorDecorator
  4. ConcreteDecorator: one concrete decorator for each error state: UnexpectedError, ServiceError, ConnectivityError

So, to recap:

  • We have a BLoC to manage the error states. It will send the error states to the views.
  • We have Decorators to enhance our widgets according with the current error state
  • We need a class to create the error decorator widget regarding the error state. I called ErrorBuilder, and it will listen the Error Bloc changes.

Let’s see how it looks like putting all together.

ErrorDecorator

import 'package:flutter/widgets.dart';
import '/src/presentation/error/bloc/error_bloc_state.dart';

abstract class ErrorDecorator extends StatelessWidget {

Widget component;
ErrorBlocState errorState;

ErrorDecorator(this.component, this.errorState, {super.key});

@override
Widget build(BuildContext context) {
return component;
}
}

UnexpectedErrorDecorator

import 'package:flutter/widgets.dart';

import 'error_decorator.dart';

class UnexpectedErrorDecorator extends ErrorDecorator {

UnexpectedErrorDecorator(super.component, super.errorState, {super.key});

@override
Widget build(BuildContext context) {

Widget child = component;

Widget enhancedWidget = Column(
children:[
const Text("I am an unexpected error decorator"),
Text(errorState?.message??""),
child
]
);

return enhancedWidget;
}
}

ServiceErrorDecorator

import 'package:flutter/widgets.dart';

import 'error_decorator.dart';

class ServiceErrorDecorator extends ErrorDecorator {

ServiceErrorDecorator(super.component, super.errorState, {super.key});

@override
Widget build(BuildContext context) {

Widget child = component;

Widget enhancedWidget = Column(
children:[
const Text("I am a service error decorator"),
Text(errorState?.message??""),
child
]
);

return enhancedWidget;
}
}

ConnectivityErrorDecorator

import 'package:flutter/widgets.dart';

import 'error_decorator.dart';

class ConnectivityErrorDecorator extends ErrorDecorator {

ConnectivityErrorDecorator(super.component, super.errorState, {super.key});

@override
Widget build(BuildContext context) {

Widget child = component;

Widget enhancedWidget = Column(
children:[
const Text("I am a connectivity error decorator"),
Text(errorState?.message??""),
child
]
);

return enhancedWidget;
}
}

ErrorBuilder

import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '/src/presentation/error/widgets/connectivity_error_decorator.dart';
import '/src/presentation/error/widgets/service_error_decorator.dart';
import '/src/presentation/error/widgets/unexpected_error_decorator.dart';

import '/src/presentation/error/bloc/error_bloc.dart';
import '/src/presentation/error/bloc/error_bloc_state.dart';

class ErrorBuilder extends StatelessWidget {

Widget widget;

ErrorBuilder(this.widget, {super.key});

@override
Widget build(BuildContext context) {

return BlocBuilder<ErrorBloc, ErrorBlocState>(
builder: (context, state) {
Widget enhanced;
switch (state.status) {
case ErrorBlocStatus.nothing:
{
enhanced = widget;
}
break;
case ErrorBlocStatus.connectivityFailure:
{
enhanced = ConnectivityErrorDecorator(widget, state);
}
break;
case ErrorBlocStatus.unexpectedFailure:
{
enhanced = UnexpectedErrorDecorator(widget, state);
}
break;
case ErrorBlocStatus.serviceFailure:
{
enhanced = ServiceErrorDecorator(widget, state);
}
break;
default:
enhanced = widget;
}
;

return enhanced;
});
}
}

Then we can use the error builder to enhance any widget, to draw any widget. Below there is a home page example:

Enhanced Widget Example

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '/src/presentation/error/widgets/error_builder.dart';


class HomePage extends StatelessWidget {

final String title;

const HomePage({super.key, required this.title});

@override
Widget build(BuildContext context) {
Widget home = Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("I am the home page"),
],
),
)
);

return ErrorBuilder(home);
}
}

Conclusion

  1. Create a BLoC to manage error states.
  2. Create Decorators to enhance your widgets.
  3. Create one concrete Decorator for each type of Error you want to manage.
  4. Use the error builder to listen the error BLoC in all the widget you want to present the errors.
  5. Always hive in mind design patterns, they’ve been solving our problems for a long time. Don’t try to reinvent the wheel.

Thanks for reading, Clap if you like it!

Photo by Wil Stewart on Unsplash

Let me know your comments below.

--

--

Bernardo Iribarne
Nerd For Tech

Passionate about design patterns, frameworks and well practices. Becoming a Flutter expert