Guides Pratiques

Gestion des erreurs

Comprendre comment Qora remonte les erreurs, configure le comportement de retry et active la dégradation gracieuse avec previousData.

Qora distingue les erreurs transitoires (retry automatique) des erreurs terminales (remontées à l'UI). Tous les échecs de fetch passent par le même pipeline : retry avec backoff exponentiel → transition vers Failure → notification des abonnés.


L'état Failure

Quand tous les retries sont épuisés, QoraState transite vers Failure :

sealed class QoraState<T> { ... }

final class Failure<T> extends QoraState<T> {
  final Object error;
  final StackTrace? stackTrace;
  final T? previousData; // non-null quand des données étaient en cache avant l'échec
}

previousData porte la dernière valeur récupérée avec succès. Cela permet la dégradation gracieuse : afficher des données périmées à côté d'un indicateur d'erreur plutôt que de remplacer toute la vue.

user_screen.dart
QoraBuilder<User>(
  queryKey: ['users', userId],
  fetcher: () => api.getUser(userId),
  builder: (context, state, fetchStatus) => switch (state) {
    Initial() => const SizedBox.shrink(),
    Loading(:final previousData) => previousData != null
        ? Stack(children: [UserCard(previousData), const LoadingOverlay()])
        : const CircularProgressIndicator(),
    Success(:final data) => UserCard(data),
    Failure(:final error, :final previousData) => previousData != null
        ? Column(children: [
            UserCard(previousData),
            ErrorBanner('Échec du rafraîchissement : $error'),
          ])
        : ErrorScreen('$error'),
  },
)

Configuration du retry

Qora relance automatiquement les fetchs échoués avec un backoff exponentiel. Contrôler cela par requête via QoraOptions :

main.dart
QoraOptions(
  retryCount: 3,                              // Défaut : 3 ; mettre 0 pour désactiver
  retryDelay: const Duration(seconds: 1),     // Délai de base ; double à chaque tentative
)

Calendrier de backoff par défaut :

TentativeDélai
11 s
22 s
34 s

Délai personnalisé

main.dart
QoraOptions(
  retryDelayCalculator: (attempt) => Duration(
    milliseconds: (500 * pow(2, attempt)).toInt(),
  ),
)

Désactiver les retries

main.dart
// Échec immédiat : ex. pour les mutations ou les requêtes temps réel
QoraOptions(retryCount: 0)

Ignorer sélectivement les retries

Pour ignorer les retries pour des types d'erreurs spécifiques (ex. 401 Unauthorized ne doit jamais être retenté) :

main.dart
QoraOptions(
  retryCount: 3,
  retryDelayCalculator: (attempt) {
    // Ne sera pas appelé si retryCount est effectivement 0,
    // mais la logique de retry peut être conditionnée dans le fetcher lui-même :
    return Duration(seconds: 1 << attempt);
  },
)

Le pattern recommandé est de lever une exception spécifique et de la capturer dans le fetcher :

user_api.dart
Future<User> getUser(int id) async {
  final response = await http.get(Uri.parse('/users/$id'));
  if (response.statusCode == 401) throw const UnauthorizedException();
  if (response.statusCode == 404) throw const NotFoundException();
  if (response.statusCode != 200) throw HttpException(response.statusCode);
  return User.fromJson(jsonDecode(response.body));
}

Puis dans QoraClientConfig.errorMapper, normaliser les erreurs avant qu'elles atteignent le moteur de retry :

main.dart
QoraClientConfig(
  errorMapper: (error, stack) {
    if (error is UnauthorizedException) return error; // Ne pas envelopper : laisser remonter
    return QoraException('$error', originalError: error, stackTrace: stack);
  },
)

Gestion globale des erreurs

Utiliser QoraClientConfig.errorMapper pour normaliser toutes les erreurs en un seul type avant qu'elles remontent dans Failure.error :

main.dart
final client = QoraClient(
  config: QoraClientConfig(
    errorMapper: (error, stackTrace) {
      if (error is DioException) {
        return switch (error.type) {
          DioExceptionType.connectionTimeout => const NetworkTimeoutError(),
          DioExceptionType.receiveTimeout => const NetworkTimeoutError(),
          _ => ApiError(error.response?.statusCode),
        };
      }
      return error;
    },
  ),
);

Erreurs de fetch en arrière-plan

Quand vous utilisez watchQuery, les fetchs en arrière-plan s'exécutent dans des futures unawaited. Si le fetch échoue, l'erreur est stockée dans l'état Failure, mais l'exception relancée ferait crasher la Zone asynchrone courante. Utilisez QoraClientConfig.onBackgroundFetchError pour intercepter ces erreurs en toute sécurité pour la journalisation ou le rapport de crash :

lib/core/qora.dart
final client = QoraClient(
  config: QoraClientConfig(
    onBackgroundFetchError: (error, stackTrace, key) {
      // L'erreur est déjà stockée dans l'état Failure, utiliser pour le reporting
      crashReporter.record(error, stackTrace);
    },
  ),
);

Le callback reçoit l'erreur, la stack trace et la clé de requête normalisée. L'erreur a déjà été stockée dans l'état Failure de la requête, ce callback est uniquement pour l'observabilité.


Observation des erreurs par tracker

Implémenter QoraTracker.onQueryFetchFailed pour observer toutes les erreurs de requête globalement : utile pour le rapport de crash ou l'analytics :

analytics_tracker.dart
class AnalyticsTracker extends NoOpTracker {
  @override
  void onQueryFetchFailed(String key, Object error, StackTrace stackTrace) {
    Sentry.captureException(error, stackTrace: stackTrace);
    analytics.track('query_error', {'key': key, 'error': '$error'});
  }
}

Erreurs de mutation

Les erreurs de mutation sont exposées sur MutationFailure :

main.dart
QoraMutationBuilder<User, UpdatePayload, User?>(
  mutator: (payload) => api.updateUser(payload),
  options: MutationOptions(
    onMutate: (payload) async =>
        QoraScope.of(context).getQueryData<User>(['users', userId]),
    onError: (error, payload, snapshot) async {
      if (snapshot != null) {
        QoraScope.of(context).restoreQueryData(['users', userId], snapshot);
      }
    },
    onSettled: (_, _, _, _) async =>
        QoraScope.of(context).invalidate(['users', userId]),
  ),
  builder: (context, state, mutate) => switch (state) {
    MutationFailure(:final error) => ErrorBanner('$error'),
    _ => UpdateButton(onTap: () => mutate(payload)),
  },
)

Erreurs hors-ligne

Quand networkMode est NetworkMode.online (défaut) et que l'appareil est hors-ligne :

  • fetchQuery lève QoraOfflineException si aucune donnée en cache n'existe.
  • watchQuery émet FetchStatus.paused : l'état reste à Loading(previousData: ...) si des données ont été précédemment mises en cache.
  • Le fetch est rejoué automatiquement quand la connectivité est restaurée.
main.dart
try {
  final data = await client.fetchQuery<User>(
    key: ['users', userId],
    fetcher: () => api.getUser(userId),
  );
} on QoraOfflineException {
  showSnackBar('Pas de connexion internet');
}

Voir aussi

Copyright © 2026