Mastering Error Handling in Flutter with Dio

Mastering Error Handling in Flutter with Dio

In mobile app development, handling errors gracefully is crucial to provide a smooth and user-friendly experience. When working with Flutter, Dio, a powerful HTTP client library, offers robust mechanisms for handling errors during network requests. In this article, we will explore how to effectively handle errors in Flutter using Dio, ensuring that your app remains reliable and user-friendly.

1. Introduction to Dio


Dio is a versatile package that simplifies making HTTP requests in Flutter applications. It provides features like cancellation, interceptors, request/response transformation, and error handling. Error handling is a critical aspect of network communication, as it allows your app to handle scenarios like no internet connection, server errors, and more.

2. Dio Configuration and Instance Creation


Before we dive into error handling, let’s set up Dio by creating an instance with appropriate configurations. We’ll define timeouts, base URLs, and other settings to tailor Dio to our app’s needs.3. Global Error Handling with Interceptors:

const String APPLICATION_JSON = "application/json";
const String CONTENT_TYPE = "content-type";
const String ACCEPT = "accept";
const String AUTHORIZATION = "authorization";
const String DEFAULT_LANGUAGE = "en";
const String TOKEN = "token";
const String BASE_URL = "https://api.example.com";

class DioFactory {

  Future<Dio> getDio() async {
    Dio dio = Dio();

    Map<String, String> headers = {
      CONTENT_TYPE: APPLICATION_JSON,
      ACCEPT: APPLICATION_JSON,
      AUTHORIZATION: TOKEN,
      DEFAULT_LANGUAGE: DEFAULT_LANGUAGE
    };

    dio.options = BaseOptions(
        baseUrl: BASE_URL,
        headers: headers,
        receiveTimeout: Constants.apiTimeOut,
        sendTimeout: Constants.apiTimeOut,
    );

    if (!kReleaseMode) {
      dio.interceptors.add(PrettyDioLogger(
        requestHeader: true,
        requestBody: true,
        responseHeader: true,
      ));
    }

    return dio;
  }
}

3. DataSource enum


This is an enumeration that defines various data sources, each associated with a specific type of failure. It to be used for mapping error types to failure responses.

enum DataSource {
  SUCCESS,
  NO_CONTENT,
  BAD_REQUEST,
  FORBIDDEN,
  UNAUTORISED,
  NOT_FOUND,
  INTERNAL_SERVER_ERROR,
  CONNECT_TIMEOUT,
  CANCEL,
  RECIEVE_TIMEOUT,
  SEND_TIMEOUT,
  CACHE_ERROR,
  NO_INTERNET_CONNECTION,
  DEFAULT
}

4. DataSourceExtension


This extension adds a method called getFailure to the DataSource enum. This method returns a Failure object based on the value of the enum.

extension DataSourceExtension on DataSource {
  Failure getFailure() {
    var mContext = navigatorKey!.currentState!.context;
    switch (this) {
      case DataSource.SUCCESS:
        return Failure(ResponseCode.SUCCESS, ResponseMessage.SUCCESS.tr(mContext));
      case DataSource.NO_CONTENT:
        return Failure(ResponseCode.NO_CONTENT, ResponseMessage.NO_CONTENT.tr(mContext));
      case DataSource.BAD_REQUEST:
        return Failure(ResponseCode.BAD_REQUEST, ResponseMessage.BAD_REQUEST.tr(mContext));
      case DataSource.FORBIDDEN:
        return Failure(ResponseCode.FORBIDDEN, ResponseMessage.FORBIDDEN.tr(mContext));
      case DataSource.UNAUTORISED:
        return Failure(ResponseCode.UNAUTORISED, ResponseMessage.UNAUTORISED.tr(mContext));
      case DataSource.NOT_FOUND:
        return Failure(ResponseCode.NOT_FOUND, ResponseMessage.NOT_FOUND.tr(mContext));
      case DataSource.INTERNAL_SERVER_ERROR:
        return Failure(ResponseCode.INTERNAL_SERVER_ERROR,
            ResponseMessage.INTERNAL_SERVER_ERROR.tr(mContext));
      case DataSource.CONNECT_TIMEOUT:
        return Failure(
            ResponseCode.CONNECT_TIMEOUT, ResponseMessage.CONNECT_TIMEOUT.tr(mContext));
      case DataSource.CANCEL:
        return Failure(ResponseCode.CANCEL, ResponseMessage.CANCEL.tr(mContext));
      case DataSource.RECIEVE_TIMEOUT:
        return Failure(
            ResponseCode.RECIEVE_TIMEOUT, ResponseMessage.RECIEVE_TIMEOUT.tr(mContext));
      case DataSource.SEND_TIMEOUT:
        return Failure(ResponseCode.SEND_TIMEOUT, ResponseMessage.SEND_TIMEOUT.tr(mContext));
      case DataSource.CACHE_ERROR:
        return Failure(ResponseCode.CACHE_ERROR, ResponseMessage.CACHE_ERROR.tr(mContext));
      case DataSource.NO_INTERNET_CONNECTION:
        return Failure(ResponseCode.NO_INTERNET_CONNECTION,
            ResponseMessage.NO_INTERNET_CONNECTION.tr(mContext));
      case DataSource.DEFAULT:
        return Failure(ResponseCode.DEFAULT, ResponseMessage.DEFAULT.tr(mContext));
    }
  }
}

5. ResponseCode class


This class defines static integer constants representing various HTTP status codes, both standard HTTP status codes and custom ones for local status codes.

class ResponseCode {
  static const int SUCCESS = 200; // success with data
  static const int NO_CONTENT = 201; // success with no data (no content)
  static const int BAD_REQUEST = 400; // failure, API rejected request
  static const int UNAUTORISED = 401; // failure, user is not authorised
  static const int FORBIDDEN = 403; //  failure, API rejected request
  static const int INTERNAL_SERVER_ERROR = 500; // failure, crash in server side
  static const int NOT_FOUND = 404; // failure, not found

  // local status code
  static const int CONNECT_TIMEOUT = -1;
  static const int CANCEL = -2;
  static const int RECIEVE_TIMEOUT = -3;
  static const int SEND_TIMEOUT = -4;
  static const int CACHE_ERROR = -5;
  static const int NO_INTERNET_CONNECTION = -6;
  static const int DEFAULT = -7;
}

6. ResponseMessage class


This class defines static string constants representing response messages for different HTTP status codes. These messages to be internationalized (using localization).

class ResponseMessage {
  static const String SUCCESS = AppStrings.success; // success with data
  static const String NO_CONTENT = AppStrings.success; // success with no data (no content)
  static const String BAD_REQUEST = AppStrings.strBadRequestError; // failure, API rejected request
  static const String UNAUTORISED = AppStrings.strUnauthorizedError; // failure, user is not authorised
  static const String FORBIDDEN = AppStrings.strForbiddenError; //  failure, API rejected request
  static const String INTERNAL_SERVER_ERROR = AppStrings.strInternalServerError; // failure, crash in server side
  static const String NOT_FOUND = AppStrings.strNotFoundError; // failure, crash in server side

  // local status code
  static const String CONNECT_TIMEOUT = AppStrings.strTimeoutError;
  static const String CANCEL = AppStrings.strDefaultError;
  static const String RECIEVE_TIMEOUT = AppStrings.strTimeoutError;
  static const String SEND_TIMEOUT = AppStrings.strTimeoutError;
  static const String CACHE_ERROR = AppStrings.strCacheError;
  static const String NO_INTERNET_CONNECTION = AppStrings.strNoInternetError;
  static const String DEFAULT = AppStrings.strDefaultError;
}

7. HandleError function


This private function takes a DioException as a parameter and returns a Failure object.It switches on the type of the DioException and maps different types of DioException to corresponding Failure values based on a set of enumerated values defined in the DataSource enum.

Failure _handleError(DioException error) {
  switch (error.type) {
    case DioExceptionType.connectionTimeout:
      return DataSource.CONNECT_TIMEOUT.getFailure();
    case DioExceptionType.sendTimeout:
      return DataSource.SEND_TIMEOUT.getFailure();
    case DioExceptionType.receiveTimeout:
      return DataSource.RECIEVE_TIMEOUT.getFailure();
    case DioExceptionType.badResponse:
      if (error.response != null &&
          error.response?.statusCode != null &&
          error.response?.statusMessage != null) {
        return Failure(error.response?.statusCode ?? 0,
            error.response?.statusMessage ?? "");
      } else {
        return DataSource.DEFAULT.getFailure();
      }
    case DioExceptionType.cancel:
      return DataSource.CANCEL.getFailure();
    default:
      return DataSource.DEFAULT.getFailure();
  }
}

7. ErrorHandler class


This class implements the Exception interface, indicating that it’s intended for handling exceptions.
It has a late field named failure of type Failure, which is not initialized immediately.
The ErrorHandler class has a constructor named handle, which takes a dynamic error parameter. It handles different types of exceptions by calling the _handleError function based on the type of the error.
If the error is of type DioException, it calls the _handleError function to determine the failure.
If the error is not a DioException, it sets the failure to a default value obtained from a data source called DataSource.

class ErrorHandler implements Exception {
  late Failure failure;

  ErrorHandler.handle(dynamic error) {
    if (error is DioException) {
      // dio error so its an error from response of the API or from dio itself
      failure = _handleError(error);
    } else {
      // default error
      failure = DataSource.DEFAULT.getFailure();
    }
  }
}

8. Handling Errors in Specific Requests


While global error handling is essential, you can also handle errors on a per-request basis. Use try-catch blocks around Dio requests to capture errors and respond accordingly.

Future<Either<Failure, ResponseDto>> getResponse(RequestDto requestDto) async {
    if (await _networkInfo.isConnected) {
      try {
        ...
        .
        .
        return Right(response);
      } catch (error) {
        return Left(ErrorHandler.handle(error).failure);
      }
    } else {
      return Left(DataSource.NO_INTERNET_CONNECTION.getFailure());
    }
  }

9. Displaying User-Friendly Error Messages


To ensure a positive user experience, convert technical error messages into user-friendly messages. You can use a helper function to map error codes to human-readable messages that guide users on how to proceed.

///English Message
"success": "success",
"bad_request_error": "bad request. try again later",
"no_content": "success with not content",
"forbidden_error": "forbidden request. try again later",
"unauthorized_error": "user unauthorized, try again later",
"not_found_error": "url not found, try again later",
"conflict_error": "conflict found, try again later",
"internal_server_error": "some thing went wrong, try again later",
"unknown_error": "some thing went wrong, try again later",
"timeout_error": "time out, try again late",
"default_error": "some thing went wrong, try again later",
"cache_error": "cache error, try again later",
"no_internet_error": "Please check your internet connection"

//Arabic Message
"success": "تم بنجاح",
"bad_request_error": "طلب غير صالح. حاول مرة أخرى لاحقًا",
"no_content": "success with not content",
"forbidden_error": "طلب محظور. حاول مرة أخرى لاحقًا",
"unauthorized_error": "user unauthorized, try again later",
"not_found_error": "url غير موجود , حاول مرة أخرى لاحقًا",
"conflict_error": "تم العثور على تعارض , حاول مرة أخرى لاحقًا",
"internal_server_error": "حدث خطأ ما , حاول مرة أخرى لاحقًا",
"unknown_error": "حدث خطأ ما , حاول مرة أخرى لاحقًا",
"timeout_error": "انتهت المهلة , حاول مرة أخرى متأخرًا",
"default_error": "حدث خطأ ما , حاول مرة أخرى لاحقًا",
"cache_error": "خطأ في ذاكرة التخزين المؤقت , حاول مرة أخرى لاحقًا",
"no_internet_error": "يُرجى التحقق من اتصالك بالإنترنت"

10. Conclusion

Effective error handling is essential for delivering a robust and reliable Flutter app. Dio’s comprehensive error handling mechanisms, combined with user-friendly error messages, ensure that users remain informed even in challenging network scenarios. By implementing these strategies, you can elevate your app’s reliability and enhance the overall user experience.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top