Implementing Comprehensive Error Handling for Network Issues in Flutter

Network operations are a fundamental part of modern mobile applications. In Flutter, handling network requests requires robust error handling to provide a seamless user experience, even when things go wrong. Comprehensive error handling includes managing various types of exceptions, displaying user-friendly error messages, and implementing retry mechanisms.

Why Comprehensive Error Handling?

  • Improved User Experience: Provides clear and helpful error messages instead of crashing or displaying technical jargon.
  • Robustness: Ensures the application gracefully handles unexpected issues like network outages or server errors.
  • Maintainability: Centralized error handling makes the codebase easier to maintain and debug.

Common Network Errors in Flutter

Before diving into the implementation, let’s identify the common network-related errors you might encounter:

  • TimeoutException: The request took too long to complete.
  • SocketException: A low-level network issue (e.g., no internet connection).
  • HttpException: General HTTP-related exception.
  • FormatException: Incorrect data format received (e.g., invalid JSON).
  • ServerErrorException: Errors that can came from the server that the user cannot handle directly from its app like 500 internal error

Implementing Comprehensive Error Handling

Step 1: Setup Dependencies

Make sure you have the http package added to your pubspec.yaml file.

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.5

Run flutter pub get to install the dependencies.

Step 2: Implement a Generic Network Request Function

Create a generic function to handle network requests with comprehensive error handling.

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';

class NetworkService {
  static const int timeoutDuration = 10; // seconds

  Future<dynamic> get(String url) async {
    return _handleRequest(() => http.get(Uri.parse(url)).timeout(const Duration(seconds: timeoutDuration)));
  }

  Future<dynamic> post(String url, {dynamic body}) async {
    return _handleRequest(() => http.post(Uri.parse(url), body: body).timeout(const Duration(seconds: timeoutDuration)));
  }

  Future<dynamic> put(String url, {dynamic body}) async {
    return _handleRequest(() => http.put(Uri.parse(url), body: body).timeout(const Duration(seconds: timeoutDuration)));
  }

  Future<dynamic> delete(String url) async {
    return _handleRequest(() => http.delete(Uri.parse(url)).timeout(const Duration(seconds: timeoutDuration)));
  }


  Future<dynamic> _handleRequest(Future<http.Response> Function() request) async {
    try {
      final response = await request();
      return _response(response);
    } on SocketException {
      throw FetchDataException('No Internet connection');
    } on TimeoutException {
      throw ApiNotRespondingException('API not responding');
    }
  }

  dynamic _response(http.Response response) {
    switch (response.statusCode) {
      case 200:
        var responseJson = json.decode(response.body.toString());
        print(responseJson);
        return responseJson;
      case 400:
        throw BadRequestException(response.body.toString());
      case 401:
      case 403:
        throw UnauthorisedException(response.body.toString());
      case 500:
      default:
        throw FetchDataException(
            'Error occurred while Communication with Server with StatusCode : ${response.statusCode}');
    }
  }
}

class CustomException implements Exception {
  final _message;
  final _prefix;
  CustomException([this._message, this._prefix]);
  String toString() {
    return "$_prefix$_message";
  }
}

class FetchDataException extends CustomException {
  FetchDataException([String? message])
      : super(message, "Error During Communication: ");
}

class BadRequestException extends CustomException {
  BadRequestException([message]) : super(message, "Invalid Request: ");
}

class UnauthorisedException extends CustomException {
  UnauthorisedException([message]) : super(message, "Unauthorised: ");
}

class InvalidInputException extends CustomException {
  InvalidInputException([String? message]) : super(message, "Invalid Input: ");
}

class ApiNotRespondingException extends CustomException {
  ApiNotRespondingException([String? message]) : super(message, "Api not responding: ");
}

Explanation:

  • Generic Methods: This code sets up a `NetworkService` class with generic methods (`get`, `post`, `put`, `delete`) to handle common HTTP requests.
  • Error Handling: Uses a central `_handleRequest` function to manage network requests. It catches common exceptions like `SocketException` (no internet) and `TimeoutException`, throwing custom exceptions for each case.
  • Custom Exceptions: Defines a set of custom exception classes (`FetchDataException`, `BadRequestException`, `UnauthorisedException`, `InvalidInputException`, `ApiNotRespondingException`) for specific error scenarios, inheriting from a base `CustomException`. This makes error handling more specific and easier to manage.
  • Response Handling: The `_response` method processes the HTTP response. It checks the status code and throws appropriate exceptions for client errors (400, 401, 403) and server errors (500 or any other unexpected status code). A status code of 200 (OK) returns the JSON-decoded response body.
  • Timeout: Configures a `timeoutDuration` for requests, set to 10 seconds. Each request is wrapped in a `timeout` function to prevent indefinite waiting.

Step 3: Using the Network Service in your App

Here’s how you might use the NetworkService class in your Flutter app, along with UI updates to show errors:


import 'package:flutter/material.dart';
import 'network_service.dart';  // Assuming the above code is in 'network_service.dart'

class ExampleScreen extends StatefulWidget {
  @override
  _ExampleScreenState createState() => _ExampleScreenState();
}

class _ExampleScreenState extends State<ExampleScreen> {
  final NetworkService networkService = NetworkService();
  String data = 'No data yet.';
  String errorMessage = '';

  Future<void> fetchData() async {
    setState(() {
      errorMessage = '';  // Clear any previous errors
    });

    try {
      final response = await networkService.get('https://jsonplaceholder.typicode.com/todos/1');
      setState(() {
        data = response['title'];  // Access the 'title' from the JSON response
      });
    } catch (e) {
      setState(() {
        errorMessage = e.toString();  // Set the error message from the exception
        data = 'Error fetching data.';  // Reset or indicate data state on error
      });
      print('Error fetching data: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Network Service Example')),
      body: Padding(
        padding: const EdgeInsets(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text('Data: $data', style: TextStyle(fontSize: 16)),
            SizedBox(height: 10),
            if (errorMessage.isNotEmpty)  // Only show error message if it exists
              Text('Error: $errorMessage', style: TextStyle(color: Colors.red)),
            ElevatedButton(
              onPressed: fetchData,
              child: Text('Fetch Data'),
            ),
          ],
        ),
      ),
    );
  }
}

Key improvements:

  • State Management with Error Display: Manages state for data, displaying it and any error messages on the screen. Utilizes `setState` to rebuild the UI with updated information.
  • Error Handling in UI: Catches exceptions during data fetching, sets an error message in the state, and displays this message in the UI, offering clear feedback to the user.
  • Conditional Error Display: Only shows the error message UI if there’s an error, avoiding empty or misleading displays.
  • Asynchronous Data Handling: Uses `async` and `await` to properly handle the asynchronous `fetchData` function, ensuring the UI updates smoothly and doesn’t block.

Additional Best Practices

  • Implement Retry Logic: For transient errors (e.g., timeouts), consider implementing a retry mechanism with exponential backoff.
  • Centralized Error Reporting: Integrate error reporting tools (e.g., Firebase Crashlytics) to monitor and address issues in production.
  • Provide Contextual Messages: Tailor error messages to be informative and actionable for the user.

Conclusion

Implementing comprehensive error handling for network issues in Flutter is crucial for creating a reliable and user-friendly application. By anticipating and handling various network errors, providing informative feedback, and using best practices like retry logic, you can ensure a seamless experience for your users, even when faced with network challenges. This comprehensive approach not only enhances user satisfaction but also makes your application more robust and easier to maintain.