Skip to content

Gestion des Erreurs en Dart

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.

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 syntaxe
void main() {
print('Hello World' // Parenthèse manquante
}
// Erreur de type
void main() {
String name = 42; // Impossible d'assigner un int à un String
}
// Erreur null safety
void 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
}

Ces erreurs ne provoquent pas de crash mais produisent des résultats incorrects :

// Calcul incorrect de la moyenne
double 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;
}

Les exceptions représentent des conditions que le programme peut gérer et dont il peut se remettre :

// Exception récupérable
void 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
}
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é');
}
}
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();
}
}
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');
}
}
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');
}
}
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(', ')}';
}
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...
}
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;
}
}
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);
}
}
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];
}
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');
}
}

Gérez les erreurs au niveau le plus approprié :

// ❌ Mauvais : gestion trop générale
void main() {
try {
processUserData();
} catch (e) {
print('Une erreur s\'est produite');
}
}
// ✅ Bon : gestion spécifique et contextuelle
class 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}');
}
}
}
// ❌ Mauvais : message vague
throw Exception('Erreur');
// ✅ Bon : message descriptif
throw 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”

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();
}
}
}

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”
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);
}
// Utilisation
Future<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}');
}
}
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écifiques
enum 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”
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 individuelle
Future<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é');
}
}
}
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é'),
);
}
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;
}
}
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;
}
}
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');
}
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');
}
}
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'));
});
});
}
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,
);
}
}
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,
);
}
}

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.