10. Error Handling

10. Error Handling

Hello, Welcome back. Glad that you’re here.

We're building a Movie App with the best coding practices and tools. Till now in UI, we've made HomeScreen, Navigation Drawer, and About Dialog.

We have till now focused on happy scenarios like making API calls that will never fail. The app will always have a happy path and no failure is a myth. So, let's handle the errors when making API calls.

DARTZ Recap

If you remember, in the Repositories & UseCase tutorial, we have used Dartz Plugin. We have used only one feature of this plugin - Either. The underlying concept is very simple -

Return Left when an error, Right when success. Left and Right are object/data holders.

If you are coming to this tutorial directly, please visit that tutorial) first and come back.

Although, I recommend that you watch the full series and all previous tutorials in this series.

In the repository layer, we used Either<Left, Right> for all the methods. When making a service call in the data source, if the service call is a success, we returned the response in the Right object. In case of any exceptions, we returned an AppError in the Left Object.

Type of Errors

Errors can be of many types. It can be service not responding, can be a parsing error. Sometimes, when you try to internet access and the internet is off, you will get an exception. For all those scenarios, we will return with the Left object.

There can be many errors that the TMDb API throws like -

  • 501 - Invalid service: this service does not exist
  • 401 - Authentication failed: You do not have permissions to access the service
  • 400 - Validation failed
  • 500 - Internal error: Something went wrong, contact TMDb.
  • 503 - The API is undergoing maintenance. Try again later.

These are just a few of the API status codes. You can get all of them from here - Status Codes

We will consider these errors as one type of error in the application and hence will give only one generic error message whenever there is any type of exception thrown by the API.

We will also handle the most popular type of error i.e. network not connected error. What do you do when you make an API call and there is no network.

There are possibly two direct solutions. First, you can stop making API calls when you know there is no internet. Second, you make the call anyway and upon SocketException you can tell the user about no internet.

To implement the first option, you need to deal with platform-specific APIs, that will observe network changes. You will also need to make UX in such a way that the user is not allowed to make any API call when there is no internet.

No matter what option you choose, you'll have to implement the second way in your app. Because imagine you make 5 subsequent calls and mid-way the user disables the network connection. So, you'll have to handle no network error anyway and intimate the user about the same.

Here, in the Movie App as well, I will show you to implement the second approach only.

App Error

If you remember in the 3rd tutorial [Repositories & UseCase], we created a class AppError that holds a strong message. Since our requirements have changed, we need to slightly update this to now accept an enum instead of a dumb String message.

Update the app_error.dart:

class AppError extends Equatable {
  //2
  final AppErrorType errorType;

  const AppError(this.errorType);

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

//1
enum AppErrorType { api, network }
  1. Create an enum with two values. api is for API exceptions and network is for no internet.
  2. Instead of String use AppErrorType now.

When you have updated the AppError class, you should get errors in the MovieRepositoryImpl. So, let's start updating these methods.

//1
on SocketException {
  return Left(AppError(AppErrorType.network));
//2
} on Exception {
  return Left(AppError(AppErrorType.api));
}
  1. Maintain the order. First, handle SocketException. You'll get this exception when the network is not connected and you make an API call. You'll return AppError with the network type.
  2. Next, you'll handle generic Exception that will capture any other exception than SocketException. We are considering that any other exception thrown when making an API call will be from API only. There can be one more exception, which can be when you're parsing JSON. But, I believe that is a logical programming mistake that can be handled while developing. So, we are still handling that exception under the api type.

Update all the remaining methods in the repository.

Error Widget

When there is an error, you need to show a text message and 2 buttons as I showcased before. We already have BlocBuilder in place in the HomeScreen. We'll handle the extra state MovieCarouselError and return CarouselLoadErrorWidget.

Open HomeScreen and add else if:

else if (state is MovieCarouselError) {
  return CarouselLoadErrorWidget(
    //1
    bloc: movieCarouselBloc,
    //2
    errorType: state.errorType,
  );
}
  1. Bloc you have already defined in the HomeScreen.
  2. AppErrorType will come from the state. We will change the state in just a moment.

In the home/movie_carousel folder, create a new file carousel_load_error_widget.dart:

import 'package:flutter/material.dart';

class CarouselLoadErrorWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
  1. Create a basic Stateless widget. And update the import in HomeScreen.

Turn off the network connection and restart the application. You'll see that we cannot see the carousel widget as the error widget is there now.

Let's build this widget now.

//1
final AppErrorType errorType;
//2
final MovieCarouselBloc bloc;

const CarouselLoadErrorWidget({
  Key key,
  @required this.errorType,
  @required this.bloc,
}) : super(key: key);
  1. The error text depends on AppErrorType so, declare a final field.
  2. When you press the retry button, you'll load the trending movies again, so declare MovieCarouselBloc to dispatch the CarouselLoadEvent later.

Come back to the HomeScreen and see there is an error. We need to add the required parameters in CarouselLoadErrorWidget. You'll easily get an instance of the bloc, but to get AppErrorType in the state, you need to pass it.

Update MovieCarouselError state:

class MovieCarouselError extends MovieCarouselState {
 //1
  final AppErrorType errorType;

  const MovieCarouselError(this.errorType);
}
  1. Declare the field for AppErrorType and introduce the constructor.

Next, update the Bloc to return the errorType:

//1
(l) => MovieCarouselError(l.appErrorType),
  1. Fetch the appErrorType from the Left that we returned from the repository methods. This appErrorType will be different when there is a network error or api error.

Before proceeding further, let's add the translation strings for error messages and the retry button.

Update the en.json, es.json and translation_constants.dart:

{
 "retry": "Retry",
  "somethingWentWrong": "Something went wrong...",
  "checkNetwork": "Please check your network connection and press Retry button or put in as a bug by pressing Feedback button.",
}
"retry": "Rever",
    "somethingWentWrong": "Algo salió mal...",
    "checkNetwork": "Verifique su conexión de red y presione el botón Reintentar o colóquelo como error presionando el botón Comentarios.",
static const String retry = 'retry';
static const String somethingWentWrong = 'somethingWentWrong';
static const String checkNetwork = 'checkNetwork';

After adding translations, you can create the UI in CarouselLoadErrorWidget:

//7
Padding(
  padding: EdgeInsets.symmetric(horizontal: Sizes.dimen_32.w),
  //1
  child: Column(
   //6
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
     //2
      Text(
        errorType == AppErrorType.api
            ? TranslationConstants.somethingWentWrong.t(context)
            : TranslationConstants.checkNetwork.t(context),
        textAlign: TextAlign.center,
        style: Theme.of(context).textTheme.subtitle1,
      ),
      //3
      ButtonBar(
        children: [
         //4
          Button(
            onPressed: () => bloc.add(CarouselLoadEvent()),
            text: TranslationConstants.retry,
          ),
          //5
          Button(
            onPressed: () => Wiredash.of(context).show(),
            text: TranslationConstants.feedback,
          ),
        ],
      ),
    ],
  ),
);
  1. Use Column to layout elements vertically.

  2. For error messages, use the Text widget. Based on errorType you'll decide the string. Make all the text-align in the center and give it subtitle1 style.

  3. We will layout the 2 buttons in the horizontal direction, so use ButtonBar.

  4. Use the Button that we created in previous tutorials. On tap of the Retry button, we will make another call to fetch the trending movies.

  5. Second button will open the Wiredash bottom sheet that we worked on in the last tutorial.

  6. To bring the content in the center, we will use MainAxisSize.max and MainAxisAlignment.center in the Column.

  7. Give some horizontal padding to avoid the content touching the screen.

Now, restart the app. To see the api error message, you can change the API_KEY in the ApiConstants class and restart the app again. You can play with different scenarios now.

Generic Error Widget

How do you handle errors in calling the APIs that are called when we switch tabs? There can be certain scenarios when the carousel has loaded successfully but individuals tabs haven't. We'll apply the same logic here as well.

If we use the existing error widget, we will need to make it so generic that it handles the operation with the tap of the Retry button. You can do that by directly passing the onPressed method in the constructor of CarouselLoadErrorWidget.

So, let's update the CarouselLoadErrorWidget first to make it more generic.

//1
final Function onPressed;

const CarouselLoadErrorWidget({
  Key key,
  @required this.errorType,
  @required this.onPressed,
}) : super(key: key);

//2
Button(
  onPressed: onPressed,
  text: TranslationConstants.retry,
),
  1. Add the onPressed as the final field.
  2. Use this in the Button now.

In the HomeScreen, instead of passing the bloc itself, pass the functionality you want to perform on the Retry button tap.

//3
CarouselLoadErrorWidget(
  onPressed: () => movieCarouselBloc.add(
    CarouselLoadEvent(),
  ),
  errorType: state.errorType,
)

Run the app again by turning off the internet first and then turn on the internet and press the Retry button. You'll see Retry working fine.

You can rename this widget and move it to the common widgets folder as it has now become generic to be used anywhere in the application.

Rename it to AppErrorWidget.

Error Handling in Tabs

Nothing much changes in terms of how the functionality will work. When you switch between tabs there is an API call made, so we will show AppErrorWidget if the API throws an error or the network is off.

In movie_tabbed_widget.dart, handle MovieTabLoadError:

//1
if (state is MovieTabLoadError)
  AppErrorWidget(
   //2
    errorType: state.errorType,
   //3
    onPressed: () => movieTabbedBloc.add(
      MovieTabChangedEvent(
        //4
        currentTabIndex: currentTabIndex,
      ),
    ),
),
  1. Handle the MovieTabLoadError state below the MovieTabChanged state.
  2. Use the AppErrorWidget and pass the errorType. We haven't updated the MovieTabLoadError with errorType, will do it just after this.
  3. Now, in onPressed() hit the MovieTabChangedEvent that will make the API call again.
  4. You would have to call the MovieTabChangedEvent with the same tab index so that the movies are fetched for the tab selected.

Now, update the MovieTabLoadError:

class MovieTabLoadError extends MovieTabbedState {
  //1
  final AppErrorType errorType;

  const MovieTabLoadError({
    int currentTabIndex,
    //2
    @required this.errorType,
  }) : super(currentTabIndex: currentTabIndex);
}
  1. Add the AppErrorType as a final field.
  2. Add this field to the constructor as a required field.

Now, you need to update the MovieTabbedBloc:

//1
(l) => MovieTabLoadError(
  currentTabIndex: event.currentTabIndex,
  errorType: l.appErrorType,
),
  1. Go to the place where you're yielding the state and update the left side by adding errorType in MovieTabLoadError.

How do you generate a scenario when trying to run this?

You can comment out the current yielding statement and yield the error state all the time. Just to test it out. Let's do it and run it.

//1
yield MovieTabLoadError(
  currentTabIndex: event.currentTabIndex,
  errorType: AppErrorType.network,
);
  1. Comment out the existing statement and add the above statement.

Now, run the app and you'll see the error message when changing tabs.

The error message should be in middle, so go to MovieTabbedWidget and add Expanded widget:

Expanded(
  child: AppErrorWidget(
    errorType: state.errorType,
    onPressed: () => movieTabbedBloc.add(
      MovieTabChangedEvent(
        currentTabIndex: state.currentTabIndex,
      ),
    ),
  ),
)

Reload and change the tab.

You can play with it in various scenarios. Uncomment the commented code and reload. Tap on the Retry button, the movies will be loaded now.

Last Error Scenario?

The scenario that I will now bring is not an error but considered as handling edge cases while coding.

What will happen if the movies you are fetching are not there and you get an empty list of movies. How the UI will work. Apply the below code in MovieTabbedBloc and run the application.

yield moviesEither.fold(
  (l) => MovieTabLoadError(
    currentTabIndex: event.currentTabIndex,
    errorType: l.appErrorType,
  ),
  (movies) {
    return MovieTabChanged(
      currentTabIndex: event.currentTabIndex,
      //1
      movies: [],
    );
  },
);
  1. Just don't pass in the movies fetched, instead pass the empty list.

If you run, you'll see no movies being shown in the movies list view.

We will show Sorry, no movies under this section when there are no movies present.

Update the en.json, es.json and translation_constants.dart:

"noMovies": "Sorry, no movies under this section"
"noMovies": "Lo siento, no hay películas en esta sección."
static const String noMovies = 'noMovies';

Now, update the MovieTabbedWidget:

//1
state.movies?.isEmpty ?? true
  //2
  ? Expanded(
      child: Center(
        child: Text(
          TranslationConstants.noMovies.t(context),
          textAlign: TextAlign.center,
          style: Theme.of(context).textTheme.subtitle1,
        ),
      ),
    )
  : Expanded(
      child: MovieListViewBuilder(movies: state.movies),
    ),
  1. In the MovieTabChanged if block, add the condition where if movies are null or empty, you show the No Movies text. We are using the ?. operator because apart from the empty movies list, the list can be null as well. If movies is null, you'll return true, which will show the No Movies text.
  2. If movies is empty or null, return an Expanded Center widget that will show the No Movies text. Use the t() extension that will translate the message in different languages.

This is how we will handle errors in future tutorials as well. I believe this tutorial has given much insight into error handling and edge case handling.

See you in the next part. n

Did you find this article valuable?

Support Prateek Sharma by becoming a sponsor. Any amount is appreciated!