# How I handle errors in Flutter

I've seen many different approaches on handling errors in Flutter projects, some even involves functional programming paradigm... Here I will show my take on how I handle errors while taking care of internationalisation (nope I'm not using `easy_localization`, I'm using the official way, with `BuildContext`). In this article I may be using the term *error* and *exception* interchangeably, though there are [some distinct differences between them](https://www.geeksforgeeks.org/errors-v-s-exceptions-in-java/), so just be aware.

My tech stack in a Flutter project usually involves `bloc`, but I think this approach can also apply to other state management approaches. Here is how it looks at the presentation layer:

```dart
class CategoriesPageGridView extends StatelessWidget {
  const CategoriesPageGridView({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CategoriesCubit, CategoriesState>(
      builder: (context, state) => switch (state) {
        CategoriesInitial() ||
        CategoriesLoading() =>
          CategoriesPageGrid.loading(context),
        CategoriesError() => Center(
            child: Text(state.getMessage(context)),
          ),
        CategoriesLoaded() => CategoriesPageGrid(
            items: state.currentData!.map((category) {
              return CategoryGridItem(
                category: category,
                onTap: () {},
              );
            }).toList(),
          ),
      },
    );
  }
}
```

The `state.getMessage(context)` function would return a `String` that contains the localised error message (e.g. `context.l10n.networkError`), and it would be rendered when the error state `CategoriesError` is emitted by the `CategoriesCubit`. Let's have a look how our `CategoriesCubit` and `CategoriesState` look:

```dart
class CategoriesCubit extends Cubit<CategoriesState> {
    CategoriesCubit({
        required BooksRepository booksRepository,
    })  : _booksRepository = booksRepository,
            super(const CategoriesInitial(null));

    final BooksRepository _booksRepository;

    void fetchCategories() async {
        try {
          emit(CategoriesLoading(state.currentData));
          final categories = await _booksRepository.fetchCategories();
          emit(CategoriesLoaded(categories));
        } on AppError catch (error) {
          emit(
            CategoriesError(
              getMessage: error.getMessage,
              currentData: state.currentData,
            ),
          );
        }
      }
    }
}
```

```dart
sealed class CategoriesState extends Equatable {
  const CategoriesState(this.currentData);

  final Iterable<Category>? currentData;

  @override
  List<Object?> get props => [currentData];
}

final class CategoriesInitial extends CategoriesState {
  const CategoriesInitial(super.currentData);
}

final class CategoriesLoading extends CategoriesState {
  const CategoriesLoading(super.currentData);
}

final class CategoriesLoaded extends CategoriesState {
  const CategoriesLoaded(super.currentData);
}

final class CategoriesError extends CategoriesState {
  const CategoriesError({
    required this.getMessage,
    required Iterable<Category>? currentData,
  }) : super(currentData);

  final String Function(BuildContext) getMessage;

  @override
  List<Object?> get props => [getMessage, currentData];
}
```

Let's put the focus on the `CategoriesError` for now. It contains a `getMessage` function that returns a `String` with a provided `BuildContext` . From the `fetchCategories()` function in the `CategoriesCubit` , you can see that it is passing the `error.getMessage` function from `AppError` to the `CategoriesError` state. But what is this `AppError` and where does it come from?

Before I get into what is `AppError`, let's see who is throwing it and how was it thrown. In the `fetchCategories()` function, the culprit who can throw the `AppError` is `await _booksRepository.fetchCategories()` , so let's have a look at the repository:

```dart
class BooksRepository {
  BooksRepository({
    required BackendApiClient apiClient,
  }) : _apiClient = apiClient;

  final BackendApiClient _apiClient;

  BooksApi get _api => _apiClient.http.getBooksApi();

Future<Iterable<Category>> fetchCategories() async {
    try {
      final response = await _api.getCategories();
      return response.data!;
    } catch (error, stackTrace) {
      AppError.throwWithStackTrace(FetchCategoriesException(error), stackTrace);
    }
  }
}
```

As you can see, an `AppError` is being thrown at a repository method whenever an exception is caught. Ideally, I will do the same for every repository method so that I can transform those exceptions into `AppError` before it was caught by the callers (in our case, it's usually the blocs/cubits).

And if you're wondering, here is the definition of `FetchCategoriesException` :

```dart
class FetchCategoriesException extends CustomException {
  const FetchCategoriesException(super.error);

  @override
  String getMessage(BuildContext context) {
    return context.l10n.failedToLoadCategoriesErrorMessage;
  }
}
```

Wait... what is this `CustomException` ?? Let me explain together with the `AppError` class:

```dart
class AppError {
  AppError({
    required this.error,
    required this.getMessage,
  });

  final Object error;
  final String Function(BuildContext) getMessage;

  static String Function(BuildContext)? _getClientErrorMessage(Object error) {
    switch (error) {
      case HandshakeException:
      case HttpException:
      case SocketException:
        return (context) => context.l10n.networkErrorMessage;

      case final DioException e:
        switch (e.error) {
          case Object _ when e.type == DioExceptionType.connectionTimeout:
          case Object _ when e.type == DioExceptionType.connectionError:
          case SocketException:
            return (context) => context.l10n.networkErrorMessage;
        }
        switch (e.response) {
          case Object _ when e.response?.statusCode == 401:
            final isTokenSentInRequest =
                e.requestOptions.headers[HttpHeaders.authorizationHeader] !=
                    null;
            if (isTokenSentInRequest) {
              throw SessionExpiredError(e);
            }
            throw UnauthorizedError(e);

          case Object _ when e.response?.statusCode == 404:
            return (context) => context.l10n.serverUnavailableErrorMessage;

          // additional cases here if you want to parse server responses
          // e.g. server validation errors
        }
      default:
        return null; // leave it to CustomException to fill this up
    }
  }

  static Never throwWithStackTrace(Object error, StackTrace stackTrace) {
    final getMessage =
        _getClientErrorMessage(error is CustomException ? error.error : error);
    Error.throwWithStackTrace(
      AppError(
        error: error is CustomException ? error.error : error,
        getMessage: getMessage ?? (context) => context.l10n.defaultErrorMessage,
      ),
      stackTrace,
    );
  }
}
```

It looks quite lengthy, because I was trying to map the exceptions thrown by both `http` and `dio` package to a user-friendly error message, plus wrapping the default `Error.throwWithStackTrace(...)` with our own so the catcher would receive the `getMessage` function that then allows our error messages to be displayed in translated languages.

Now there are still two missing pieces - what is `CustomException` and why were `SessionExpiredError` and `UnauthorizedError` thrown?

```dart
abstract class CustomException implements Exception {
  const CustomException(this.error);

  /// The error which was caught.
  final Object error;

  /// User-friendly message to show.
  String getMessage(BuildContext context);
}

class UnauthorizedError extends CustomException {
  const UnauthorizedError(Object error) : super(error);

  @override
  String getMessage(BuildContext context) {
    return context.l10n.unauthorizedErrorMessage;
  }
}

class SessionExpiredError extends CustomException {
  const SessionExpiredError(Object error) : super(error);

  @override
  String getMessage(BuildContext context) {
    return context.l10n.sessionExpiredErrorMessage;
  }
}
```

Turned out `CustomException` is just a base class I created, that implements the `Exception` class, so that it contains a localised message getter `getMessage(BuildContext context)`. The `UnauthorizedError` and `SessionExpiredError` were created the same way as the `FetchCategoriesException` .

These two errors were intentionally thrown by `AppError` because I intend to handle them at the global level:

```dart
PlatformDispatcher.instance.onError = (error, stack) {
    if (error is UnauthorizedError) {
      appBloc.add(const UnauthorizedEvent());
    } else if (error is SessionExpiredError) {
      appBloc.add(const SessionExpiredEvent());
    } else {
      log('🔥 $error', stackTrace: stack);
      // or call Sentry for error reporting
    }
    return true;
  };
```

Tada! I let the `PlatformDispatcher` catch the two uncaught errors so that I can add the corresponding event to my top-level `AppBloc` , which is responsible for handling top-level events in the whole app. I can then use a `BlocListener` to show a snackbar, dialog, and/or perform navigations. The possibilities are endless.

So far this is working pretty well for me... What do you think? Share your thoughts and maybe your approach with me in the comments :)
