Flutter & Dio: Handling API Errors with Custom Dialog
In modern mobile application development, interacting with APIs is a fundamental requirement. Whether fetching data, submitting forms, or performing authentication, robust API communication is essential. However, network requests are inherently prone to failures due to various factors like internet connectivity issues, server downtime, or invalid requests. Effectively handling these API errors and providing clear, actionable feedback to users is crucial for a great user experience.
This article will guide you through implementing a professional error handling mechanism in your Flutter applications using Dio, a powerful HTTP client, combined with custom dialogs to display user-friendly error messages.
Why Custom Error Dialogs?
When an API call fails, simply logging the error to the console isn't enough. Users need to understand what went wrong and, if possible, how to resolve it. A custom error dialog offers several advantages:
- Improved User Experience: Provides immediate visual feedback.
- Clear Messaging: Allows tailoring error messages to be user-friendly, rather than exposing raw technical errors.
- Consistency: Ensures all API errors are presented in a unified manner throughout the application.
- Actionable Information: Can suggest solutions (e.g., "Check your internet connection") or direct users to support.
Setting Up Dio in Flutter
First, ensure you have Dio added to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
dio: ^5.0.0 # Use the latest version available
Then, run flutter pub get. Next, let's create a Dio instance, potentially with some base options:
import 'package:dio/dio.dart';
// Create a global Dio instance or within a service
final Dio dio = Dio(BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com', // Replace with your actual base API URL
connectTimeout: const Duration(seconds: 5), // 5 seconds
receiveTimeout: const Duration(seconds: 3), // 3 seconds
));
We'll use jsonplaceholder.typicode.com as a public API for our examples, which allows us to simulate both successful and failed requests.
Understanding DioError (DioException) Types
In Dio versions 5.x and later, DioError has been renamed to DioException. When an API request fails, Dio throws a DioException, which contains a type property that helps categorize the error. Understanding these types is key to providing specific error messages:
DioExceptionType.connectionTimeout: The request took too long to connect to the server.DioExceptionType.sendTimeout: The client failed to send the request data in time.DioExceptionType.receiveTimeout: The client failed to receive the response data in time.DioExceptionType.badResponse: The server responded with a status code outside the 2xx range (e.g., 404 Not Found, 500 Internal Server Error). This type replacedDioExceptionType.responsein newer versions.DioExceptionType.cancel: The request was cancelled.DioExceptionType.badConnection: An error occurred during the connection (e.g., no internet). This type replacedDioExceptionType.otherfor network issues in newer versions.DioExceptionType.unknown: An unexpected error occurred.
Designing a Custom Error Dialog
Let's create a reusable function to display our custom error dialog. This function will take the BuildContext and an error message as arguments.
import 'package:flutter/material.dart';
void showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('Error'),
content: Text(message),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(dialogContext).pop();
},
),
],
);
},
);
}
Integrating Custom Dialog with Dio Error Handling
Now, let's put it all together. We'll wrap our Dio calls in a try-catch block to handle DioException and then use our showErrorDialog function.
Example: Fetching Data with Error Handling
Consider a simple Flutter screen with a button that triggers an API call. We'll demonstrate how to handle various error types and display appropriate messages.
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
// Assume showErrorDialog function is defined as above or imported from a utility file
// Global Dio instance (for simplicity in this example)
final Dio _dio = Dio(BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com', // Base URL for JSONPlaceholder
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 3),
));
// Reusable function to show error dialog (defined globally or in a utility)
void showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('Error'),
content: Text(message),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(dialogContext).pop();
},
),
],
);
},
);
}
class ApiErrorDemoScreen extends StatefulWidget {
const ApiErrorDemoScreen({super.key});
@override
State createState() => _ApiErrorDemoScreenState();
}
class _ApiErrorDemoScreenState extends State {
String _data = 'No data yet.';
bool _isLoading = false;
Future _fetchPosts() async {
setState(() {
_isLoading = true;
_data = 'Loading...';
});
try {
// --- Example 1: Successful API Call ---
final successResponse = await _dio.get('/posts/1');
setState(() {
_data = 'Success: ${successResponse.data['title']}';
});
// --- Example 2: Simulating a 404 Not Found Error ---
// Try to fetch a resource that doesn't exist
// The DioException will be caught, and the dialog will show 'The requested resource was not found.'
final errorResponse = await _dio.get('/posts/999999');
setState(() {
_data = 'Should not reach here if 404 occurs.';
});
} on DioException catch (e) {
String errorMessage = 'An unknown error occurred.';
if (e.type == DioExceptionType.badResponse) {
// Server responded with a status code other than 2xx
int? statusCode = e.response?.statusCode;
String? responseMessage = e.response?.data['message'] ?? e.response?.statusMessage;
if (statusCode == 404) {
errorMessage = 'The requested resource was not found.';
} else if (statusCode == 401 || statusCode == 403) {
errorMessage = 'Authentication/Authorization failed. Please log in again.';
} else if (statusCode! >= 500) {
errorMessage = 'Server is currently unavailable. Please try again later.';
} else {
errorMessage = 'API Error: Status $statusCode - ${responseMessage ?? 'Unknown error'}';
}
} else if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout ||
e.type == DioExceptionType.sendTimeout) {
errorMessage = 'Connection timed out. Please check your internet connection.';
} else if (e.type == DioExceptionType.badConnection) { // For network issues (Dio 5.x+)
errorMessage = 'No internet connection. Please check your network.';
} else if (e.type == DioExceptionType.cancel) {
errorMessage = 'Request was cancelled.';
} else {
// Other types of errors (e.g., parsing error, other network issues)
errorMessage = 'Network Error: ${e.message ?? 'Please check your internet connection.'}';
}
showErrorDialog(context, errorMessage);
setState(() {
_data = 'Error occurred. Check dialog.';
});
} catch (e) {
// Catch any other unexpected errors
showErrorDialog(context, 'An unexpected error occurred: ${e.toString()}');
setState(() {
_data = 'Unexpected error occurred.';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('API Error Handling Demo'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_data, textAlign: TextAlign.center, style: const TextStyle(fontSize: 18)),
const SizedBox(height: 20),
_isLoading
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: _fetchPosts,
child: const Text('Fetch Data (and test errors)'),
),
],
),
),
),
);
}
}
Explanation of the Error Handling Logic:
- The
_fetchPostsfunction makes two API calls: one for a successful fetch and another (/posts/999999) that will reliably return a 404 Not Found error from JSONPlaceholder. - The
on DioException catch (e)block specifically handles errors thrown by Dio. - Inside this block, we check
e.typeto differentiate between various error scenarios.DioExceptionType.badResponse: We further inspecte.response?.statusCodeto provide specific messages for 404, 401/403, and 5xx errors.DioExceptionType.connectionTimeout,receiveTimeout,sendTimeout: Indicate network slowness.DioExceptionType.badConnection: Suggests no active internet connection.- Other types are caught and handled generally.
- The generic
catch (e)block ensures that any non-Dio related exceptions are also caught and handled, preventing app crashes. - Finally,
showErrorDialog(context, errorMessage)is called with the tailored message.
Advanced Error Handling with Dio Interceptors
For larger applications, repeating the try-catch logic in every API call can become cumbersome. Dio's Interceptors provide a centralized way to handle requests, responses, and errors globally. You can create a custom interceptor that automatically catches DioException, processes it, and displays the custom dialog.
Implementing an interceptor requires a way to access BuildContext from outside a widget tree (e.g., using a global NavigatorKey or a service locator pattern to inject a dialog service). While powerful for global error handling, the `try-catch` approach demonstrated above is often sufficient and easier to implement for specific error handling needs, especially when the context is readily available.
Conclusion
Handling API errors gracefully is paramount for developing robust and user-friendly Flutter applications. By combining Dio's powerful HTTP capabilities with custom error dialogs, you can provide clear, actionable feedback to your users, significantly enhancing their experience even when things go wrong.
The approach outlined in this article allows you to categorize different types of API failures and present tailored messages, transforming cryptic technical errors into understandable information. Remember to continuously refine your error messages based on user feedback to make your application as intuitive as possible.