Gestion des Erreurs en Dart
Introduction à la gestion des erreurs
Section titled “Introduction à la gestion des erreurs”La gestion des erreurs est un aspect crucial du développement d’applications robustes en Dart. Une gestion appropriée des erreurs permet de créer des applications plus fiables et d’améliorer l’expérience utilisateur en gérant gracieusement les situations exceptionnelles.
Types d’erreurs en Dart
Section titled “Types d’erreurs en Dart”1. Erreurs de compilation (Compile-time errors)
Section titled “1. Erreurs de compilation (Compile-time errors)”Ces erreurs sont détectées par l’analyseur Dart avant l’exécution du programme :
// Erreur de syntaxevoid main() { print('Hello World' // Parenthèse manquante}
// Erreur de typevoid main() { String name = 42; // Impossible d'assigner un int à un String}
// Erreur null safetyvoid main() { String? name; print(name.length); // Erreur : name peut être null}2. Erreurs d’exécution (Runtime errors)
Section titled “2. Erreurs d’exécution (Runtime errors)”Ces erreurs surviennent pendant l’exécution du programme :
void main() { List<int> numbers = [1, 2, 3]; print(numbers[5]); // RangeError : index hors limites
int? value; print(value!); // Null check operator sur une valeur null
String text = "abc"; int number = int.parse(text); // FormatException}3. Erreurs logiques (Logic errors)
Section titled “3. Erreurs logiques (Logic errors)”Ces erreurs ne provoquent pas de crash mais produisent des résultats incorrects :
// Calcul incorrect de la moyennedouble calculateAverage(List<int> numbers) { int sum = 0; for (var number in numbers) { sum += number; } // Erreur logique : division par zéro si la liste est vide return sum / numbers.length;}Distinction Exception vs Error
Section titled “Distinction Exception vs Error”Exception
Section titled “Exception”Les exceptions représentent des conditions que le programme peut gérer et dont il peut se remettre :
// Exception récupérablevoid processUserInput(String input) { try { int age = int.parse(input); if (age < 0) { throw ArgumentError('L\'âge ne peut pas être négatif'); } print('Âge valide : $age'); } on FormatException { print('Format d\'âge invalide'); } on ArgumentError catch (e) { print('Erreur d\'argument : ${e.message}'); }}Les erreurs représentent des problèmes graves qui ne devraient généralement pas être interceptés :
void main() { try { // Stack overflow - Error grave recursiveFunction(); } catch (e) { // Généralement, on ne devrait pas intercepter les Error print('Erreur grave : $e'); }}
void recursiveFunction() { recursiveFunction(); // StackOverflowError}Try-Catch-Finally
Section titled “Try-Catch-Finally”Syntaxe de base
Section titled “Syntaxe de base”void demonstrateTryCatch() { try { // Code pouvant lever une exception int result = 10 ~/ 0; // Division entière par zéro print('Résultat : $result'); } catch (e) { // Gestion de toutes les exceptions print('Une erreur s\'est produite : $e'); } finally { // Code exécuté dans tous les cas print('Nettoyage terminé'); }}Capture spécifique d’exceptions
Section titled “Capture spécifique d’exceptions”void specificCatchExample() { try { List<String> data = fetchDataFromAPI(); processData(data); } on HttpException catch (e) { print('Erreur HTTP : ${e.message}'); } on FormatException catch (e) { print('Erreur de format : ${e.message}'); } on Exception catch (e) { print('Exception générale : $e'); } catch (e, stackTrace) { print('Erreur inattendue : $e'); print('Stack trace : $stackTrace'); } finally { cleanupResources(); }}Try-catch avec async/await
Section titled “Try-catch avec async/await”Future<void> asyncErrorHandling() async { try { String data = await fetchRemoteData(); await processAsyncData(data); } on TimeoutException { print('Timeout lors de la récupération des données'); } on SocketException { print('Problème de connexion réseau'); } catch (e) { print('Erreur asynchrone : $e'); }}Lancer des exceptions personnalisées
Section titled “Lancer des exceptions personnalisées”Exceptions personnalisées simples
Section titled “Exceptions personnalisées simples”class CustomException implements Exception { final String message; const CustomException(this.message);
@override String toString() => 'CustomException: $message';}
void validateAge(int age) { if (age < 0) { throw CustomException('L\'âge ne peut pas être négatif'); } if (age > 150) { throw CustomException('L\'âge semble irréaliste'); }}Exceptions avec plus de contexte
Section titled “Exceptions avec plus de contexte”class ValidationException implements Exception { final String field; final String message; final dynamic value;
const ValidationException({ required this.field, required this.message, this.value, });
@override String toString() => 'ValidationException($field): $message (valeur: $value)';}
class UserRegistrationError implements Exception { final String email; final List<String> errors;
const UserRegistrationError(this.email, this.errors);
@override String toString() => 'Erreur d\'inscription pour $email: ${errors.join(', ')}';}Exceptions communes en Dart
Section titled “Exceptions communes en Dart”ArgumentError
Section titled “ArgumentError”void calculateDiscount(double price, double percentage) { if (price < 0) { throw ArgumentError('Le prix ne peut pas être négatif'); } if (percentage < 0 || percentage > 100) { throw ArgumentError.value(percentage, 'percentage', 'Doit être entre 0 et 100'); } // Calcul de la remise...}StateError
Section titled “StateError”class ShoppingCart { List<String> _items = []; bool _isCheckedOut = false;
void addItem(String item) { if (_isCheckedOut) { throw StateError('Impossible d\'ajouter des articles après validation'); } _items.add(item); }
void checkout() { if (_items.isEmpty) { throw StateError('Impossible de valider un panier vide'); } _isCheckedOut = true; }}FormatException
Section titled “FormatException”int parseUserAge(String input) { try { return int.parse(input); } on FormatException { throw FormatException('Format d\'âge invalide', input); }}
DateTime parseCustomDate(String dateString) { try { return DateTime.parse(dateString); } on FormatException { throw FormatException('Format de date invalide. Utilisez YYYY-MM-DD', dateString); }}RangeError
Section titled “RangeError”T getElementAt<T>(List<T> list, int index) { if (index < 0 || index >= list.length) { throw RangeError.range(index, 0, list.length - 1, 'index'); } return list[index];}UnsupportedError
Section titled “UnsupportedError”class ReadOnlyList<T> { final List<T> _items;
ReadOnlyList(this._items);
T operator [](int index) => _items[index];
void operator []=(int index, T value) { throw UnsupportedError('Cette liste est en lecture seule'); }}Bonnes pratiques de gestion des erreurs
Section titled “Bonnes pratiques de gestion des erreurs”1. Principe de proximité
Section titled “1. Principe de proximité”Gérez les erreurs au niveau le plus approprié :
// ❌ Mauvais : gestion trop généralevoid main() { try { processUserData(); } catch (e) { print('Une erreur s\'est produite'); }}
// ✅ Bon : gestion spécifique et contextuelleclass UserService { Future<User> createUser(String email, String password) async { try { validateEmail(email); validatePassword(password); return await userRepository.create(email, password); } on ValidationException catch (e) { throw UserCreationException('Données invalides : ${e.message}'); } on DatabaseException catch (e) { throw UserCreationException('Erreur de sauvegarde : ${e.message}'); } }}2. Messages d’erreur informatifs
Section titled “2. Messages d’erreur informatifs”// ❌ Mauvais : message vaguethrow Exception('Erreur');
// ✅ Bon : message descriptifthrow ArgumentError( 'Le paramètre "email" doit être une adresse email valide. ' 'Reçu: "$email". Format attendu: utilisateur@domaine.com');3. Utilisation des types d’exceptions appropriés
Section titled “3. Utilisation des types d’exceptions appropriés”class BankAccount { double _balance;
BankAccount(this._balance);
void withdraw(double amount) { // Validation des arguments if (amount <= 0) { throw ArgumentError.value(amount, 'amount', 'Le montant doit être positif'); }
// Vérification de l'état if (amount > _balance) { throw StateError('Solde insuffisant. Solde actuel: $_balance, demandé: $amount'); }
_balance -= amount; }}Quand intercepter vs laisser remonter les erreurs
Section titled “Quand intercepter vs laisser remonter les erreurs”Intercepter les erreurs
Section titled “Intercepter les erreurs”Interceptez quand :
- Vous pouvez récupérer de l’erreur
- Vous devez transformer l’erreur
- Vous devez ajouter du contexte
- Vous devez effectuer un nettoyage
class DataService { Future<List<User>> fetchUsers() async { try { final response = await httpClient.get('/users'); return parseUsers(response.body); } on SocketException { // Récupération possible : mode hors ligne return await localDatabase.getCachedUsers(); } on HttpException catch (e) { // Transformation de l'erreur avec contexte throw DataServiceException('Impossible de récupérer les utilisateurs: ${e.message}'); } finally { // Nettoyage nécessaire await httpClient.close(); } }}Laisser remonter les erreurs
Section titled “Laisser remonter les erreurs”Laissez remonter quand :
- Vous ne pouvez pas gérer l’erreur de manière significative
- L’appelant est mieux placé pour décider
- Vous ne faites que du traitement intermédiaire
class UserRepository { Future<User> findById(String id) async { // Validation simple, mais laisse remonter les erreurs de database if (id.isEmpty) { throw ArgumentError('L\'ID utilisateur ne peut pas être vide'); }
// Laisse remonter DatabaseException - l'appelant décidera return await database.query('SELECT * FROM users WHERE id = ?', [id]); }}Patterns Result/Either pour la gestion d’erreurs
Section titled “Patterns Result/Either pour la gestion d’erreurs”Pattern Result simple
Section titled “Pattern Result simple”abstract class Result<T> { const Result();
bool get isSuccess => this is Success<T>; bool get isError => this is Error<T>;
T get value => (this as Success<T>).value; String get error => (this as Error<T>).message;}
class Success<T> extends Result<T> { final T value; const Success(this.value);}
class Error<T> extends Result<T> { final String message; const Error(this.message);}
// UtilisationFuture<Result<User>> fetchUser(String id) async { try { final user = await userApi.getUser(id); return Success(user); } on HttpException catch (e) { return Error('Erreur réseau: ${e.message}'); } on FormatException { return Error('Données utilisateur invalides'); } catch (e) { return Error('Erreur inattendue: $e'); }}
void handleUserFetch() async { final result = await fetchUser('123');
if (result.isSuccess) { print('Utilisateur: ${result.value.name}'); } else { print('Erreur: ${result.error}'); }}Pattern Either plus avancé
Section titled “Pattern Either plus avancé”abstract class Either<L, R> { const Either();
bool get isLeft => this is Left<L, R>; bool get isRight => this is Right<L, R>;
L get left => (this as Left<L, R>).value; R get right => (this as Right<L, R>).value;
T fold<T>(T Function(L left) leftFn, T Function(R right) rightFn) { return isLeft ? leftFn(left) : rightFn(right); }}
class Left<L, R> extends Either<L, R> { final L value; const Left(this.value);}
class Right<L, R> extends Either<L, R> { final R value; const Right(this.value);}
// Utilisation avec des types d'erreurs spécifiquesenum UserError { notFound, networkError, invalidData }
Future<Either<UserError, User>> fetchUserEither(String id) async { try { final user = await userApi.getUser(id); if (user == null) { return const Left(UserError.notFound); } return Right(user); } on SocketException { return const Left(UserError.networkError); } on FormatException { return const Left(UserError.invalidData); }}Gestion d’erreurs dans le code asynchrone
Section titled “Gestion d’erreurs dans le code asynchrone”Futures multiples
Section titled “Futures multiples”Future<void> handleMultipleFutures() async { try { // Exécution parallèle avec gestion d'erreurs final results = await Future.wait([ fetchUserData(), fetchUserPreferences(), fetchUserStats(), ]);
processResults(results); } catch (e) { print('Une ou plusieurs opérations ont échoué: $e'); }}
// Alternative avec gestion individuelleFuture<void> handleMultipleFuturesIndividually() async { final futures = [ fetchUserData().catchError((e) => null), fetchUserPreferences().catchError((e) => null), fetchUserStats().catchError((e) => null), ];
final results = await Future.wait(futures);
// Traiter les résultats en gérant les valeurs null for (int i = 0; i < results.length; i++) { if (results[i] == null) { print('L\'opération $i a échoué'); } }}Streams avec gestion d’erreurs
Section titled “Streams avec gestion d’erreurs”Stream<String> processDataStream() async* { try { await for (final data in inputStream) { try { final processed = await processItem(data); yield processed; } catch (e) { // Gestion d'erreur par élément print('Erreur lors du traitement de $data: $e'); yield 'ERREUR: $data'; } } } catch (e) { // Erreur du stream global yield* Stream.error('Erreur critique du stream: $e'); }}
void listenToStream() { processDataStream().listen( (data) => print('Données reçues: $data'), onError: (error) => print('Erreur stream: $error'), onDone: () => print('Stream terminé'), );}Timeout et cancellation
Section titled “Timeout et cancellation”Future<String> fetchWithTimeout() async { try { return await fetchRemoteData() .timeout(const Duration(seconds: 10)); } on TimeoutException { throw Exception('La requête a pris trop de temps'); }}
Future<String> fetchWithCancellation(CancelToken token) async { try { final future = fetchRemoteData();
// Vérification périodique d'annulation final result = await Future.any([ future, token.future.then((_) => throw CancellationException()), ]);
return result; } on CancellationException { print('Opération annulée'); rethrow; }}Techniques de débogage
Section titled “Techniques de débogage”1. Logging stratégique
Section titled “1. Logging stratégique”import 'dart:developer' as developer;
class Logger { static void info(String message) { developer.log(message, level: 800); }
static void warning(String message) { developer.log(message, level: 900); }
static void error(String message, [Object? error, StackTrace? stackTrace]) { developer.log( message, level: 1000, error: error, stackTrace: stackTrace, ); }}
Future<User> fetchUser(String id) async { Logger.info('Début de récupération utilisateur: $id');
try { final user = await userApi.getUser(id); Logger.info('Utilisateur récupéré avec succès: ${user.name}'); return user; } catch (e, stackTrace) { Logger.error('Erreur lors de la récupération utilisateur', e, stackTrace); rethrow; }}2. Assertions pour le développement
Section titled “2. Assertions pour le développement”void calculateDiscount(double price, double percentage) { assert(price >= 0, 'Le prix doit être positif'); assert(percentage >= 0 && percentage <= 100, 'Pourcentage invalide');
// Calcul seulement si les assertions passent final discount = price * (percentage / 100); print('Remise: $discount');}3. Gestion des erreurs non gérées
Section titled “3. Gestion des erreurs non gérées”void main() { // Gestion globale des erreurs non interceptées FlutterError.onError = (FlutterErrorDetails details) { FlutterError.presentError(details); // Envoyer à un service de monitoring reportError(details.exception, details.stack); };
// Gestion des erreurs des zones isolées runZonedGuarded(() { runApp(MyApp()); }, (error, stackTrace) { print('Erreur non gérée: $error'); reportError(error, stackTrace); });}
void reportError(Object error, StackTrace? stackTrace) { // Envoi vers un service comme Sentry, Crashlytics, etc. print('Rapport d\'erreur: $error'); if (stackTrace != null) { print('Stack trace: $stackTrace'); }}4. Tests d’erreurs
Section titled “4. Tests d’erreurs”import 'package:test/test.dart';
void main() { group('Tests de gestion d\'erreurs', () { test('devrait lever ArgumentError pour âge négatif', () { expect( () => validateAge(-1), throwsA(isA<ArgumentError>()), ); });
test('devrait gérer les erreurs de format', () async { expect( () async => await parseUserData('invalid-json'), throwsA(isA<FormatException>()), ); });
test('devrait retourner une erreur dans Result', () async { final result = await fetchUserResult('invalid-id');
expect(result.isError, isTrue); expect(result.error, contains('utilisateur introuvable')); }); });}Outils de monitoring et reporting
Section titled “Outils de monitoring et reporting”1. Integration avec des services externes
Section titled “1. Integration avec des services externes”class ErrorReporter { static void reportException( Object exception, StackTrace stackTrace, { Map<String, dynamic>? context, }) { // Sentry Sentry.captureException( exception, stackTrace: stackTrace, withScope: (scope) { if (context != null) { context.forEach((key, value) { scope.setTag(key, value.toString()); }); } }, );
// Firebase Crashlytics FirebaseCrashlytics.instance.recordError( exception, stackTrace, context: context, ); }}2. Métriques d’erreurs
Section titled “2. Métriques d’erreurs”class ErrorMetrics { static final Map<String, int> _errorCounts = {};
static void incrementError(String errorType) { _errorCounts[errorType] = (_errorCounts[errorType] ?? 0) + 1; }
static Map<String, int> getErrorStats() => Map.from(_errorCounts);
static void logErrorWithMetrics(Object error, StackTrace stackTrace) { final errorType = error.runtimeType.toString(); incrementError(errorType);
Logger.error( 'Erreur ($errorType) - Occurrence #${_errorCounts[errorType]}', error, stackTrace, ); }}Ressources et documentation
Section titled “Ressources et documentation”- Documentation officielle Dart - Gestion des erreurs
- Documentation officielle Dart - Asynchronie et erreurs
- Dart API - Exception class
- Dart API - Error class
- Guide des bonnes pratiques Dart
La gestion efficace des erreurs en Dart nécessite une compréhension des différents types d’erreurs, l’utilisation appropriée des mécanismes de gestion d’exceptions, et l’adoption de bonnes pratiques pour créer des applications robustes et maintenables.