Skip to main content

11 posts tagged with "api"

View All Tags

Xử Lý Lỗi API (401, 403, 500) và Hiển Thị Thông Báo Cho Người Dùng

· 11 min read

Xử lý lỗi API một cách chuyên nghiệp là yếu tố quan trọng để tạo trải nghiệm người dùng tốt. Bài viết này sẽ hướng dẫn bạn cách xử lý các lỗi API phổ biến và hiển thị thông báo thân thiện cho người dùng.


1️⃣ Các Loại Lỗi API Phổ Biến

1.1 HTTP Status Codes

Status CodeÝ NghĩaNguyên Nhân
400Bad RequestDữ liệu gửi lên không hợp lệ
401UnauthorizedChưa đăng nhập hoặc token hết hạn
403ForbiddenKhông có quyền truy cập
404Not FoundTài nguyên không tồn tại
500Internal Server ErrorLỗi server
502Bad GatewayGateway lỗi
503Service UnavailableService không khả dụng

1.2 Network Errors

  • Connection timeout
  • No internet connection
  • DNS resolution failed
  • SSL certificate error

2️⃣ Tạo Custom Exception Classes

2.1 Base Exception

abstract class AppException implements Exception {
final String message;
final int? statusCode;

AppException(this.message, [this.statusCode]);

@override
String toString() => message;
}

2.2 Specific Exceptions

// Bad Request (400)
class BadRequestException extends AppException {
BadRequestException([String? message])
: super(message ?? 'Yêu cầu không hợp lệ', 400);
}

// Unauthorized (401)
class UnauthorizedException extends AppException {
UnauthorizedException([String? message])
: super(message ?? 'Chưa đăng nhập hoặc phiên đăng nhập đã hết hạn', 401);
}

// Forbidden (403)
class ForbiddenException extends AppException {
ForbiddenException([String? message])
: super(message ?? 'Bạn không có quyền truy cập tài nguyên này', 403);
}

// Not Found (404)
class NotFoundException extends AppException {
NotFoundException([String? message])
: super(message ?? 'Không tìm thấy tài nguyên', 404);
}

// Server Error (500)
class ServerException extends AppException {
ServerException([String? message])
: super(message ?? 'Lỗi server. Vui lòng thử lại sau.', 500);
}

// Network Error
class NetworkException extends AppException {
NetworkException([String? message])
: super(message ?? 'Không có kết nối internet. Vui lòng kiểm tra lại.', null);
}

// Timeout
class TimeoutException extends AppException {
TimeoutException([String? message])
: super(message ?? 'Kết nối timeout. Vui lòng thử lại.', null);
}

// Unknown Error
class UnknownException extends AppException {
UnknownException([String? message])
: super(message ?? 'Đã xảy ra lỗi không xác định', null);
}

3️⃣ Xử Lý Lỗi Trong Dio Interceptor

3.1 Error Interceptor

import 'package:dio/dio.dart';

class ErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
AppException exception;

switch (err.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
exception = TimeoutException('Kết nối timeout. Vui lòng thử lại.');
break;

case DioExceptionType.badResponse:
final statusCode = err.response?.statusCode;
final message = _extractErrorMessage(err.response?.data);

switch (statusCode) {
case 400:
exception = BadRequestException(message);
break;
case 401:
exception = UnauthorizedException(message);
break;
case 403:
exception = ForbiddenException(message);
break;
case 404:
exception = NotFoundException(message);
break;
case 500:
case 502:
case 503:
exception = ServerException(message);
break;
default:
exception = ServerException('Lỗi server: $statusCode');
}
break;

case DioExceptionType.cancel:
exception = UnknownException('Request đã bị hủy');
break;

case DioExceptionType.unknown:
if (err.message?.contains('SocketException') ?? false) {
exception = NetworkException();
} else {
exception = UnknownException(err.message);
}
break;

default:
exception = UnknownException(err.message);
}

handler.reject(
DioException(
requestOptions: err.requestOptions,
error: exception,
type: err.type,
response: err.response,
),
);
}

String? _extractErrorMessage(dynamic data) {
if (data is Map<String, dynamic>) {
return data['message'] as String? ??
data['error'] as String?;
} else if (data is String) {
return data;
}
return null;
}
}

3.2 Sử Dụng Interceptor

final dio = Dio();

dio.interceptors.add(ErrorInterceptor());

// Khi gọi API, lỗi sẽ được map thành custom exception
try {
final response = await dio.get('/api/data');
} on DioException catch (e) {
if (e.error is AppException) {
final exception = e.error as AppException;
// Xử lý exception
_handleError(exception);
}
}

4️⃣ Error Handler Service

4.1 Centralized Error Handler

class ErrorHandler {
static void handleError(AppException exception) {
// Log error
_logError(exception);

// Show user-friendly message
_showErrorMessage(exception);
}

static void _logError(AppException exception) {
// Log to crashlytics, sentry, etc.
print('Error: ${exception.message} (${exception.statusCode})');
}

static void _showErrorMessage(AppException exception) {
// Get current context
final context = NavigationService.navigatorKey.currentContext;
if (context == null) return;

// Show snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(exception.message),
backgroundColor: _getErrorColor(exception),
duration: Duration(seconds: 3),
action: SnackBarAction(
label: 'Đóng',
textColor: Colors.white,
onPressed: () {},
),
),
);
}

static Color _getErrorColor(AppException exception) {
if (exception is UnauthorizedException) {
return Colors.orange;
} else if (exception is ForbiddenException) {
return Colors.red;
} else if (exception is ServerException) {
return Colors.red.shade700;
} else if (exception is NetworkException) {
return Colors.blue;
} else {
return Colors.grey;
}
}

// Get user-friendly message
static String getUserMessage(AppException exception) {
return exception.message;
}

// Check if error is retryable
static bool isRetryable(AppException exception) {
return exception is NetworkException ||
exception is TimeoutException ||
exception is ServerException;
}
}

5️⃣ Hiển Thị Thông Báo Lỗi Trong UI

5.1 Snackbar

void showErrorSnackbar(BuildContext context, AppException exception) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.error_outline, color: Colors.white),
SizedBox(width: 12),
Expanded(
child: Text(
exception.message,
style: TextStyle(color: Colors.white),
),
),
],
),
backgroundColor: Colors.red,
duration: Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
action: SnackBarAction(
label: 'Đóng',
textColor: Colors.white,
onPressed: () {},
),
),
);
}

5.2 Dialog

Future<void> showErrorDialog(
BuildContext context,
AppException exception, {
VoidCallback? onRetry,
}) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.error_outline, color: Colors.red),
SizedBox(width: 8),
Text('Lỗi'),
],
),
content: Text(exception.message),
actions: [
if (onRetry != null && ErrorHandler.isRetryable(exception))
TextButton(
onPressed: () {
Navigator.of(context).pop();
onRetry();
},
child: Text('Thử lại'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Đóng'),
),
],
),
);
}

5.3 Custom Error Widget

class ErrorWidget extends StatelessWidget {
final AppException exception;
final VoidCallback? onRetry;

const ErrorWidget({
Key? key,
required this.exception,
this.onRetry,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_getErrorIcon(),
size: 64,
color: Colors.red,
),
SizedBox(height: 16),
Text(
_getErrorTitle(),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
exception.message,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
if (onRetry != null && ErrorHandler.isRetryable(exception)) ...[
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRetry,
icon: Icon(Icons.refresh),
label: Text('Thử lại'),
),
],
],
),
),
);
}

IconData _getErrorIcon() {
if (exception is NetworkException) {
return Icons.wifi_off;
} else if (exception is UnauthorizedException) {
return Icons.lock_outline;
} else if (exception is ServerException) {
return Icons.error_outline;
} else {
return Icons.warning_amber_rounded;
}
}

String _getErrorTitle() {
if (exception is NetworkException) {
return 'Không có kết nối';
} else if (exception is UnauthorizedException) {
return 'Phiên đăng nhập hết hạn';
} else if (exception is ServerException) {
return 'Lỗi server';
} else {
return 'Đã xảy ra lỗi';
}
}
}

6️⃣ Xử Lý Lỗi Trong Repository/Service

6.1 Repository với Error Handling

class UserRepository {
final Dio dio;

UserRepository(this.dio);

Future<User> getUser(int id) async {
try {
final response = await dio.get('/users/$id');
return User.fromJson(response.data as Map<String, dynamic>);
} on DioException catch (e) {
if (e.error is AppException) {
throw e.error as AppException;
}
throw UnknownException('Failed to get user');
}
}

Future<List<User>> getUsers() async {
try {
final response = await dio.get('/users');
return (response.data as List<dynamic>)
.map((json) => User.fromJson(json as Map<String, dynamic>))
.toList();
} on DioException catch (e) {
if (e.error is AppException) {
throw e.error as AppException;
}
throw UnknownException('Failed to get users');
}
}
}

6.2 Service với Error Handling

class ApiService {
final Dio dio;
final ErrorHandler errorHandler;

ApiService(this.dio, this.errorHandler);

Future<T> handleRequest<T>(
Future<Response> Function() request,
T Function(dynamic data) parser,
) async {
try {
final response = await request();
return parser(response.data);
} on DioException catch (e) {
if (e.error is AppException) {
final exception = e.error as AppException;
errorHandler.handleError(exception);
throw exception;
}
final exception = UnknownException('Request failed');
errorHandler.handleError(exception);
throw exception;
} catch (e) {
if (e is AppException) {
errorHandler.handleError(e);
}
rethrow;
}
}
}

7️⃣ Xử Lý Lỗi Trong Widget/UI

7.1 Sử Dụng FutureBuilder

class UserListWidget extends StatelessWidget {
final UserRepository userRepository;

const UserListWidget({Key? key, required this.userRepository}) : super(key: key);

@override
Widget build(BuildContext context) {
return FutureBuilder<List<User>>(
future: userRepository.getUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}

if (snapshot.hasError) {
final error = snapshot.error;
if (error is AppException) {
return ErrorWidget(
exception: error,
onRetry: () {
// Retry logic
},
);
}
return ErrorWidget(
exception: UnknownException('Đã xảy ra lỗi'),
onRetry: () {
// Retry logic
},
);
}

if (snapshot.hasData) {
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return UserListItem(user: users[index]);
},
);
}

return SizedBox.shrink();
},
);
}
}

7.2 Sử Dụng try-catch trong Event Handler

class UserScreen extends StatefulWidget {
@override
_UserScreenState createState() => _UserScreenState();
}

class _UserScreenState extends State<UserScreen> {
final UserRepository userRepository = UserRepository(ApiService.dio);
User? user;
bool isLoading = false;
AppException? error;

@override
void initState() {
super.initState();
_loadUser();
}

Future<void> _loadUser() async {
setState(() {
isLoading = true;
error = null;
});

try {
final loadedUser = await userRepository.getUser(1);
setState(() {
user = loadedUser;
isLoading = false;
});
} on AppException catch (e) {
setState(() {
error = e;
isLoading = false;
});

// Show error message
ErrorHandler.handleError(e);
} catch (e) {
final exception = UnknownException('Đã xảy ra lỗi không xác định');
setState(() {
error = exception;
isLoading = false;
});
ErrorHandler.handleError(exception);
}
}

@override
Widget build(BuildContext context) {
if (isLoading) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}

if (error != null) {
return Scaffold(
body: ErrorWidget(
exception: error!,
onRetry: _loadUser,
),
);
}

if (user == null) {
return Scaffold(
body: Center(child: Text('Không có dữ liệu')),
);
}

return Scaffold(
appBar: AppBar(title: Text('User')),
body: UserDetailsWidget(user: user!),
);
}
}

8️⃣ Xử Lý 401 - Refresh Token

8.1 Refresh Token Interceptor

class RefreshTokenInterceptor extends Interceptor {
final Dio refreshDio;
final TokenService tokenService;

RefreshTokenInterceptor(this.refreshDio, this.tokenService);

@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
try {
// Refresh token
final newToken = await tokenService.refreshToken();

if (newToken != null) {
// Retry original request
final opts = err.requestOptions;
opts.headers['Authorization'] = 'Bearer $newToken';

final response = await refreshDio.request(
opts.path,
options: Options(
method: opts.method,
headers: opts.headers,
),
data: opts.data,
queryParameters: opts.queryParameters,
);

return handler.resolve(response);
} else {
// Refresh failed, logout
await tokenService.logout();
throw UnauthorizedException('Phiên đăng nhập đã hết hạn');
}
} catch (e) {
if (e is AppException) {
return handler.reject(
DioException(
requestOptions: err.requestOptions,
error: e,
),
);
}
}
}

return handler.next(err);
}
}

9️⃣ Best Practices

9.1 Centralized Error Handling

  • ✅ Tạo một ErrorHandler service duy nhất
  • ✅ Map tất cả lỗi thành custom exceptions
  • ✅ Log errors để debug
  • ✅ Hiển thị thông báo thân thiện với người dùng

9.2 User-Friendly Messages

  • ✅ Tránh technical jargon
  • ✅ Giải thích rõ ràng vấn đề
  • ✅ Đưa ra hướng giải quyết nếu có thể
  • ✅ Cung cấp nút "Thử lại" khi phù hợp

9.3 Error Logging

class ErrorLogger {
static void logError(AppException exception, {StackTrace? stackTrace}) {
// Log to console
print('Error: ${exception.message}');
if (stackTrace != null) {
print('Stack trace: $stackTrace');
}

// Log to crashlytics/sentry
// FirebaseCrashlytics.instance.recordError(exception, stackTrace);
}
}

🔟 Kết Luận

Xử lý lỗi API chuyên nghiệp giúp:

Trải nghiệm người dùng tốt hơn: Thông báo rõ ràng, dễ hiểu
Dễ debug: Log errors đầy đủ
Code sạch hơn: Xử lý lỗi tập trung
Maintainable: Dễ thêm/xóa/sửa error handling

💡 Lời khuyên: Luôn xử lý lỗi một cách graceful. Đừng để app crash vì lỗi API. Hãy hiển thị thông báo thân thiện và cho phép người dùng thử lại.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, Error Handling, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, error handling và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.