CodeGym /Cours /JAVA 25 SELF /Gestion des erreurs dans le code asynchrone : exceptional...

Gestion des erreurs dans le code asynchrone : exceptionally, handle

JAVA 25 SELF
Niveau 55 , Leçon 3
Disponible

1. Problème : exceptions dans le code asynchrone

Dans le code classique (synchrone), c’est simple : si une exception survient dans une méthode, elle « remonte » la pile d’appels et nous pouvons la capturer à l’aide de try-catch. Par exemple :

try {
    int x = 1 / 0;
} catch (ArithmeticException ex) {
    System.out.println("Division par zéro !");
}

Dans le code asynchrone, la situation est plus complexe. Lorsque nous lançons une tâche via CompletableFuture.supplyAsync, elle s’exécute dans un autre thread. Si une exception s’y produit, elle ne sera pas propagée dans le thread principal ! À la place, elle sera « emballée » dans l’objet CompletableFuture, et si vous appelez ensuite get() ou join(), vous recevrez cette exception sous la forme d’ExecutionException.

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    // Oups, erreur ici !
    return 1 / 0;
});

try {
    Integer result = future.get(); // ici une exception sera levée !
} catch (Exception ex) {
    System.out.println("Une erreur s'est produite : " + ex.getMessage());
}

Mais si vous n’appelez pas get() (ce qui, soit dit en passant, n’est pas très asynchrone) et que vous construisez des chaînes via thenApply et d’autres méthodes, l’erreur peut « se perdre ». C’est pourquoi, en programmation asynchrone, il est très important de savoir intercepter et traiter les erreurs directement dans les chaînes CompletableFuture.

2. Méthode exceptionally : gestion des erreurs et valeur de retour

La méthode exceptionally vous permet d’intercepter une exception si elle est survenue dans les étapes précédentes de la chaîne, de la traiter et de renvoyer une valeur de remplacement. C’est comme un catch, mais pour un flux de données asynchrone.

Signature :

CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

Exemple d’utilisation

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("Nous effectuons un calcul risqué...");
    if (Math.random() > 0.5) {
        throw new RuntimeException("Quelque chose s'est mal passé !");
    }
    return 42;
});

future = future.exceptionally(ex -> {
    System.out.println("Une erreur s'est produite : " + ex.getMessage());
    return 0; // On renvoie une valeur « sûre »
});

Exemple avec thenAccept

future.thenAccept(result -> System.out.println("Résultat : " + result));

Sortie (exemple) :

Nous effectuons un calcul risqué...
Une erreur s'est produite : Quelque chose s'est mal passé !
Résultat : 0
Nous effectuons un calcul risqué...
Résultat : 42

Important ! La méthode exceptionally ne s’active que si une exception non gérée s’est produite dans la chaîne avant elle. Si tout se passe bien, elle se contente de « laisser passer » le résultat plus loin.

3. Méthode handle : gestionnaire universel du résultat et de l’erreur

Parfois, nous devons traiter à la fois le résultat et l’erreur. Par exemple, si tout va bien — renvoyer le résultat ; en cas d’erreur — renvoyer une alternative ou journaliser l’erreur.

Signature :

CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
  • Premier argument — le résultat (ou null s’il y a eu une erreur),
  • Deuxième — l’exception (ou null si tout va bien).

Exemple d’utilisation

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Erreur aléatoire !");
    return 100;
});

CompletableFuture<Integer> safeFuture = future.handle((result, ex) -> {
    if (ex != null) {
        System.out.println("Erreur détectée : " + ex.getMessage());
        return -1;
    }
    return result;
});

safeFuture.thenAccept(r -> System.out.println("Résultat final : " + r));

Sortie :

Erreur détectée : Erreur aléatoire !
Résultat final : -1
Résultat final : 100

handle est à utiliser lorsque vous voulez agir quel que soit le dénouement de la tâche — succès ou erreur. C’est un gestionnaire universel des résultats, toujours appelé, qui reçoit deux arguments : le résultat (si tout va bien) et l’exception (si quelque chose s’est mal passé).

La méthode est idéale si vous devez journaliser les erreurs de manière centralisée, renvoyer une valeur par défaut sans interrompre la chaîne, ou simplement terminer proprement un scénario asynchrone.

Exemple :

CompletableFuture<Integer> future = CompletableFuture
    .supplyAsync(() -> 10 / 0) // une erreur se produira ici
    .handle((result, ex) -> {
        if (ex != null) {
            System.out.println("Erreur : " + ex.getMessage());
            return 0; // valeur par défaut
        }
        return result;
    });

System.out.println(future.join()); // affichera 0

À la différence de exceptionally, qui réagit uniquement aux erreurs, handle s’exécute toujours, ce qui permet de traiter les deux issues au même endroit et de préserver la fluidité de toute la chaîne.

4. Méthode whenComplete : effets secondaires après la fin

Parfois, nous n’avons pas besoin de modifier le résultat, mais simplement d’exécuter une action après la fin de la tâche — par exemple, journaliser que la tâche s’est terminée, que ce soit un succès ou un échec.

Signature :

CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
  • Premier argument — le résultat (ou null en cas d’erreur),
  • Deuxième — l’exception (ou null en cas de succès).

Exemple d’utilisation

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Erreur !");
    return 10;
});

future.whenComplete((result, ex) -> {
    if (ex != null) {
        System.out.println("Erreur lors de l'exécution : " + ex.getMessage());
    } else {
        System.out.println("Terminé avec succès, résultat : " + result);
    }
});

Différence importante :
whenComplete ne modifie ni le résultat ni l’erreur, il exécute seulement une action. Si une exception se produit dans whenComplete, elle sera « attachée » à celle déjà existante.

Exemple : journaliser sans intervenir

future
    .whenComplete((res, ex) -> {
        System.out.println("Tâche terminée. Erreur ? " + (ex != null));
    })
    .thenAccept(r -> System.out.println("Résultat pour l'utilisateur : " + r));

5. Particularités et subtilités d’implémentation

Bonnes pratiques : comment bien gérer les erreurs dans CompletableFuture

  • Toujours ajouter une gestion des erreurs (exceptionally, handle ou whenComplete) dans les chaînes de tâches asynchrones. Sinon, l’erreur peut passer inaperçue et l’application se comporter de manière imprévisible.
  • N’utilisez pas get() ou join() dans le thread principal sans try-catch — cela transforme le code asynchrone en code synchrone et peut entraîner des blocages.
  • Si vous devez renvoyer une valeur « de secours » en cas d’erreur — utilisez exceptionally ou handle.
  • Pour les effets secondaires (journalisation, notification à l’utilisateur) — utilisez whenComplete.
  • Vous pouvez combiner les méthodes dans une chaîne : par exemple, d’abord traiter l’erreur via exceptionally, puis journaliser via whenComplete, puis poursuivre le traitement du résultat.
  • N’oubliez pas que si l’erreur n’est pas traitée, elle « se propagera » jusqu’au prochain appel à get()/join() et peut provoquer l’arrêt de l’application.

Ordre des méthodes

  • Si vous utilisez exceptionally, il n’intercepte que les erreurs survenues avant lui dans la chaîne.
  • Si, après exceptionally, une nouvelle erreur survient dans la chaîne (par exemple dans thenApply), elle doit être gérée séparément.
  • handle est universel — il s’exécute toujours, qu’il y ait eu erreur ou non.

Combiner les méthodes

CompletableFuture.supplyAsync(() -> {
    // ...
})
.handle((result, ex) -> {
    if (ex != null) return "Erreur : " + ex.getMessage();
    return result;
})
.whenComplete((res, ex) -> {
    System.out.println("La tâche est terminée, résultat : " + res);
});

Que se passe-t-il si l’erreur n’est pas traitée ?

Si l’exception n’est pas gérée et que vous appelez get() ou join(), elle sera levée sous forme d’ExecutionException (ou de CompletionException), et l’application peut se terminer par une erreur.

6. Erreurs courantes lors de la gestion des erreurs dans CompletableFuture

Erreur n°1: absence de gestion des erreurs. Si vous n’ajoutez ni exceptionally, ni handle, ni whenComplete, l’erreur se « perdra » jusqu’au prochain appel à get()/join(), qui peut être loin de l’endroit où elle est survenue.

Erreur n°2: utilisation de get()/join() dans le thread principal sans try-catch. Cela rend le code asynchrone synchrone et peut entraîner des blocages ou des arrêts inattendus de l’application.

Erreur n°3: mauvaise compréhension de l’endroit où le gestionnaire s’exécute. exceptionally n’intercepte que les erreurs qui se produisent avant lui dans la chaîne. Si une erreur survient après, elle ne sera pas traitée par cette méthode.

Erreur n°4: traiter l’erreur sans renvoyer de valeur. Dans la méthode exceptionally ou handle, vous devez impérativement renvoyer une valeur, sinon l’étape suivante recevra null (ou rien).

Erreur n°5: confusion entre handle et whenComplete. handle peut modifier le résultat, tandis que whenComplete exécute seulement une action (par exemple, la journalisation). Si vous voulez modifier le résultat — utilisez handle.

Erreur n°6: duplication de la logique de gestion des erreurs. Il est souvent possible de regrouper la gestion des erreurs en un seul endroit pour éviter les duplications — par exemple, via un handle centralisé ou un gestionnaire commun.

1
Mission
JAVA 25 SELF, niveau 55, leçon 3
Bloqué
Rapports du capteur : traitement flexible des données avec récupération
Rapports du capteur : traitement flexible des données avec récupération
1
Mission
JAVA 25 SELF, niveau 55, leçon 3
Bloqué
Surveillance du système : journalisation détaillée des événements
Surveillance du système : journalisation détaillée des événements
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION