Error Handling
Handling network errors gracefully — from HTTP status codes to user-friendly error messages.
When your app makes network requests, things can go wrong . The server might be down, the user might have no internet, or the request might timeout. Proper error handling is essential for:
- User experience – Show friendly error messages instead of crashing
- Debugging – Log errors for debugging and monitoring
- Recovery – Allow users to retry failed operations
- Reliability – Build apps that fail gracefully
⚠️ The Cost of Poor Error Handling
- ❌ App crashes when a network request fails
- ❌ Users see confusing error messages
- ❌ No way to recover from errors
- ❌ Hard to debug issues in production
Understanding HTTP status codes is crucial for proper error handling. Here are the most common ones:
Here's how to handle different status codes in your Flutter app with user-friendly messages.
import 'dart:convert';
import 'package:http/http.dart' as http;
class ApiException implements Exception {
final String message;
final int? statusCode;
ApiException(this.message, {this.statusCode});
@override
String toString() => 'ApiException: $message';
}
class ApiService {
final http.Client client;
ApiService({http.Client? client}) : client = client ?? http.Client();
Future<Map<String, dynamic>> fetchData() async {
try {
final response = await client.get(
Uri.parse('https://api.example.com/data'),
headers: {'Accept': 'application/json'},
);
// Handle different status codes
switch (response.statusCode) {
case 200:
// Success
return jsonDecode(response.body) as Map<String, dynamic>;
case 400:
// Bad Request
throw ApiException(
'Invalid request data. Please check your input.',
statusCode: 400,
);
case 401:
// Unauthorized
throw ApiException('Please log in to continue.', statusCode: 401);
case 403:
// Forbidden
throw ApiException(
'You don\'t have permission to access this resource.',
statusCode: 403,
);
case 404:
// Not Found
throw ApiException(
'The requested resource was not found.',
statusCode: 404,
);
case 429:
// Too Many Requests
throw ApiException(
'Too many requests. Please wait a moment and try again.',
statusCode: 429,
);
case var code when code >= 500 && code < 600:
// Server Errors
throw ApiException(
'Server error. Please try again later.',
statusCode: response.statusCode,
);
default:
throw ApiException(
'Unexpected error: ${response.statusCode}',
statusCode: response.statusCode,
);
}
} on http.ClientException catch (e) {
// Network errors (no internet, connection refused, etc.)
throw ApiException('Network error: ${e.message}');
}
}
}
Network errors happen when the device can't reach the server. Common causes include:
- No internet connection
- Server is down
- Connection timeout
- DNS resolution failure
class NetworkErrorHandler {
static String getUserFriendlyMessage(Object error) {
if (error is ApiException) {
return error.message;
}
if (error is http.ClientException) {
if (error.message.contains('Failed host lookup')) {
return 'Unable to connect to the server. Please check your internet connection.';
}
if (error.message.contains('Connection refused')) {
return 'The server is not responding. Please try again later.';
}
if (error.message.contains('SocketException')) {
return 'Network error. Please check your internet connection.';
}
if (error.message.contains('Timeout')) {
return 'The request timed out. Please check your internet connection.';
}
return 'A network error occurred. Please try again.';
}
return 'An unexpected error occurred. Please try again.';
}
}
⏱️ Timeouts
Always set timeouts for network requests. A request that hangs indefinitely is a poor user experience.
Use
timeout()
on your Future to set a maximum wait time.
final response = await client
.get(url)
.timeout(
Duration(seconds: 10),
onTimeout: () => throw ApiException('Request timed out'),
);
Here's how to display errors in your Flutter UI with a
FutureBuilder
.
import 'package:flutter/material.dart';
import 'error_handling.dart';
class DataScreen extends StatefulWidget {
@override
State<DataScreen> createState() => _DataScreenState();
}
class _DataScreenState extends State<DataScreen> {
late Future<Map<String, dynamic>> futureData;
final ApiService _apiService = ApiService();
@override
void initState() {
super.initState();
futureData = _apiService.fetchData();
}
void _retry() {
setState(() {
futureData = _apiService.fetchData();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Data')),
body: FutureBuilder<Map<String, dynamic>>(
future: futureData,
builder: (context, snapshot) {
// --- Loading State ---
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading data...'),
],
),
);
}
// --- Error State ---
if (snapshot.hasError) {
final errorMessage = NetworkErrorHandler.getUserFriendlyMessage(
snapshot.error!,
);
return Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.red.shade400,
),
SizedBox(height: 16),
Text(
'Something went wrong',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
errorMessage,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 16,
),
),
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _retry,
icon: Icon(Icons.refresh),
label: Text('Try Again'),
),
],
),
),
);
}
// --- Data State ---
if (snapshot.hasData) {
return Center(child: Text('Data loaded successfully!'));
}
// --- Fallback ---
return Center(child: Text('No data available'));
},
),
);
}
}
ApiException
class that holds
a user-friendly message and optional status code.
switch
statement or conditionals to
handle different HTTP status codes with appropriate messages.
http.ClientException
to handle
network connectivity issues.
FutureBuilder
to show loading,
error, and data states with user-friendly messages.
✅ Do's
- Always handle loading, error, and data states
- Show user-friendly error messages (not technical jargon)
- Provide a way to retry failed operations
- Log errors for debugging and monitoring
- Set timeouts for network requests
- Check status codes before parsing responses
❌ Don'ts
- Don't ignore errors or swallow exceptions
- Don't show raw error messages (like stack traces) to users
- Don't assume the network is always available
- Don't parse responses without checking status codes
- Don't forget to handle network timeouts
Here's a complete app that demonstrates all error handling concepts with a simulated API.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
// ----- Custom Exception -----
class ApiException implements Exception {
final String message;
final int? statusCode;
ApiException(this.message, {this.statusCode});
@override
String toString() => 'ApiException: $message';
}
// ----- API Service with Error Handling -----
class UserService {
final http.Client client;
UserService({http.Client? client}) : client = client ?? http.Client();
Future<List<dynamic>> fetchUsers() async {
try {
final response = await client
.get(
Uri.parse('https://jsonplaceholder.typicode.com/users'),
headers: {'Accept': 'application/json'},
)
.timeout(
Duration(seconds: 10),
onTimeout: () {
throw ApiException(
'Request timed out. Please check your connection.',
);
},
);
switch (response.statusCode) {
case 200:
return jsonDecode(response.body) as List<dynamic>;
case 401:
throw ApiException(
'Please log in to view this content.',
statusCode: 401,
);
case 404:
throw ApiException('User data not found.', statusCode: 404);
case var code when code >= 500 && code < 600:
throw ApiException(
'Server error. Please try again later.',
statusCode: response.statusCode,
);
default:
throw ApiException(
'Unexpected error: ${response.statusCode}',
statusCode: response.statusCode,
);
}
} on http.ClientException catch (e) {
throw ApiException('Network error: ${e.message}');
} catch (e) {
throw ApiException('An unexpected error occurred');
}
}
}
// ----- Error Message Helper -----
String getErrorMessage(Object error) {
if (error is ApiException) {
return error.message;
}
return 'An unexpected error occurred. Please try again.';
}
// ----- Flutter UI -----
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Error Handling Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: UserListScreen(),
);
}
}
class UserListScreen extends StatefulWidget {
@override
State<UserListScreen> createState() => _UserListScreenState();
}
class _UserListScreenState extends State<UserListScreen> {
late Future<List<dynamic>> futureUsers;
final UserService _service = UserService();
@override
void initState() {
super.initState();
futureUsers = _service.fetchUsers();
}
void _retry() {
setState(() {
futureUsers = _service.fetchUsers();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('👤 Users'),
centerTitle: true,
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _retry,
tooltip: 'Refresh',
),
],
),
body: FutureBuilder<List<dynamic>>(
future: futureUsers,
builder: (context, snapshot) {
// --- Loading ---
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading users...'),
],
),
);
}
// --- Error ---
if (snapshot.hasError) {
final errorMessage = getErrorMessage(snapshot.error!);
return Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.red.shade400,
),
SizedBox(height: 16),
Text(
'Unable to load data',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
errorMessage,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 16,
),
),
SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: _retry,
icon: Icon(Icons.refresh),
label: Text('Retry'),
),
SizedBox(width: 12),
if (snapshot.error is ApiException &&
snapshot.error.statusCode == 401)
ElevatedButton.icon(
onPressed: () {
// Navigate to login
},
icon: Icon(Icons.login),
label: Text('Log In'),
),
],
),
],
),
),
);
}
// --- Data ---
if (snapshot.hasData) {
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: CircleAvatar(child: Text((index + 1).toString())),
title: Text(user['name'] ?? 'Unknown'),
subtitle: Text(user['email'] ?? 'No email'),
),
);
},
);
}
// --- Fallback ---
return Center(child: Text('No data available'));
},
),
);
}
}
Always check
response.statusCode
before parsing the response body.
A 404 or 500 response may not contain the expected JSON structure.
Use
if (response.statusCode == 200)
before parsing, and throw exceptions
for other status codes.
Don't show
snapshot.error.toString()
directly. Users don't need to see stack traces.
Map errors to user-friendly messages like "Network error. Please check your internet connection."
If a request fails, users should be able to retry without restarting the app.
Add a "Retry" button in the error state that re-fetches the data.
🎯 Key Takeaway
Error handling is essential for a good user experience. Always handle loading, error, and data states. Show user-friendly messages and provide a retry mechanism . Handle HTTP status codes properly and don't forget about network errors and timeouts. Users will forgive errors if they understand what went wrong and can recover.