API Error Handling & Exception Management
API Error Handling & Exception Management
Robust networking code must anticipate failure. In the real world, HTTP requests fail for many different reasons: the server returns a 404 or 500, the device loses Wi-Fi mid-request, or the server simply takes too long to respond. Treating all of these as the same generic error produces unusable apps. This lesson teaches you to distinguish error categories, catch specific DioException types, and build a reusable error-handling layer that surfaces meaningful messages to the UI.
Two Distinct Failure Categories
Before writing any code, understand the fundamental split:
- Network-level errors — the HTTP response never arrives. Examples: device is offline, DNS lookup fails, the server closes the TCP connection, or the request times out waiting for a response.
- HTTP error status codes — the server did respond, but with a non-2xx status. Examples:
401 Unauthorized,403 Forbidden,404 Not Found,422 Unprocessable Entity,500 Internal Server Error.
Dio models this split perfectly. A network-level failure throws a DioException with type DioExceptionType.connectionError, connectionTimeout, or receiveTimeout. An HTTP error throws a DioException with type DioExceptionType.badResponse and a non-null response property containing the status code and body.
validateStatus is left at its default, Dio automatically throws a DioException for any response whose status code falls outside 2xx. You never have to check response.statusCode manually after a successful await — if it returned, the status was valid.DioException Types You Must Handle
DioExceptionType.connectionTimeout— took too long to establish the connection.DioExceptionType.sendTimeout— took too long to upload the request body.DioExceptionType.receiveTimeout— connected successfully but the server was too slow sending back data.DioExceptionType.connectionError— network is unreachable, DNS failed, or the connection was reset.DioExceptionType.badResponse— server replied with a non-2xx status; inspecte.response!.statusCode.DioExceptionType.cancel— the request was cancelled programmatically via aCancelToken.DioExceptionType.unknown— anything else (e.g. a JSON parse error thrown inside an interceptor).
Example 1 — Catching DioException Types Inline
import 'package:dio/dio.dart';
Future<void> fetchUser(int id) async {
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
));
try {
final response = await dio.get('/users/$id');
final user = response.data as Map<String, dynamic>;
print('Got user: ${user['name']}');
} on DioException catch (e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
print('Request timed out. Check your connection.');
break;
case DioExceptionType.connectionError:
print('No internet connection.');
break;
case DioExceptionType.badResponse:
final status = e.response?.statusCode;
if (status == 401) {
print('Unauthorised — please log in again.');
} else if (status == 404) {
print('User not found.');
} else if (status != null && status >= 500) {
print('Server error ($status). Try again later.');
}
break;
case DioExceptionType.cancel:
print('Request was cancelled.');
break;
default:
print('Unexpected error: ${e.message}');
}
}
}
Building a Reusable Error-Handling Layer
Scattering switch statements across every repository method is fragile and impossible to maintain. The better pattern is a central mapping function — often called ApiException or NetworkException — that converts a DioException into a clean, app-specific domain exception. Your repository then catches DioException once and rethrows a domain exception; the UI layer only ever sees domain exceptions.
Example 2 — Reusable Error Layer with Domain Exceptions
// 1. Define domain exception types
sealed class ApiException implements Exception {
const ApiException(this.message);
final String message;
}
class NetworkException extends ApiException {
const NetworkException() : super('No internet connection.');
}
class TimeoutException extends ApiException {
const TimeoutException() : super('The request timed out.');
}
class UnauthorisedException extends ApiException {
const UnauthorisedException() : super('Session expired. Please log in.');
}
class NotFoundException extends ApiException {
const NotFoundException(String resource)
: super('$resource was not found.');
}
class ServerException extends ApiException {
const ServerException(int code) : super('Server error ($code).');
}
// 2. Central converter function
ApiException dioToApiException(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return const TimeoutException();
case DioExceptionType.connectionError:
return const NetworkException();
case DioExceptionType.badResponse:
final status = e.response?.statusCode ?? 0;
if (status == 401) return const UnauthorisedException();
if (status == 404) return const NotFoundException('Resource');
return ServerException(status);
default:
return ApiException('Unknown error: ${e.message}');
}
}
// 3. Repository wraps every call
class UserRepository {
final Dio _dio;
UserRepository(this._dio);
Future<Map<String, dynamic>> getUser(int id) async {
try {
final res = await _dio.get('/users/$id');
return res.data as Map<String, dynamic>;
} on DioException catch (e) {
throw dioToApiException(e); // rethrow as domain exception
}
}
}
Surfacing Errors in the UI
With domain exceptions in place, the UI layer simply catches ApiException and maps each subtype to a user-friendly message. A common pattern is to hold either data or an error message in your state class:
- On
NetworkException— show "Check your connection" with a retry button. - On
TimeoutException— show "Server is slow, try again" with a retry button. - On
UnauthorisedException— redirect to the login screen. - On
NotFoundException— show a 404 illustration. - On
ServerException— show a generic error with a support contact.
sealed classes (Dart 3+) for your domain exceptions. The compiler will warn you if a switch or pattern-match is missing a subtype, making it impossible to silently swallow new error cases.Summary
A professional error-handling strategy in Flutter follows these steps:
- Configure realistic timeouts in
BaseOptionsso requests do not hang forever. - Catch
DioExceptionat the repository layer and examinee.typeto distinguish network errors from HTTP errors. - For
badResponse, inspecte.response!.statusCodeto handle 401, 404, 5xx, etc. individually. - Convert
DioExceptioninto domain exceptions immediately, keeping higher layers free of Dio imports. - Display meaningful, friendly messages in the UI based on the exception type — and always provide a way for the user to retry.