CodeGym /Kurse /JAVA 25 SELF /Fehlerbehandlung in asynchronem Code: exceptionally, hand...

Fehlerbehandlung in asynchronem Code: exceptionally, handle

JAVA 25 SELF
Level 55 , Lektion 3
Verfügbar

1. Problem: Ausnahmen in asynchronem Code

In gewöhnlichem (synchronem) Code ist es einfach: Wenn in einer Methode eine Ausnahme auftritt, propagiert sie den Aufrufstack nach oben, und wir können sie mit try-catch abfangen. Zum Beispiel:

try {
    int x = 1 / 0;
} catch (ArithmeticException ex) {
    System.out.println("Division durch Null!");
}

In asynchronem Code ist die Situation komplizierter. Wenn wir eine Aufgabe über CompletableFuture.supplyAsync starten, läuft sie in einem anderen Thread. Wenn dort eine Ausnahme auftritt, wird sie nicht im Hauptthread ausgelöst! Stattdessen wird sie in ein CompletableFuture-Objekt „verpackt“, und wenn Sie später get() oder join() aufrufen, erhalten Sie diese Ausnahme als ExecutionException.

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    // Ups, hier ist ein Fehler!
    return 1 / 0;
});

try {
    Integer result = future.get(); // hier wird eine Ausnahme ausgelöst!
} catch (Exception ex) {
    System.out.println("Es ist ein Fehler aufgetreten: " + ex.getMessage());
}

Wenn Sie jedoch get() nicht aufrufen (was an sich nicht besonders asynchron ist), sondern Ketten mit thenApply und anderen Methoden aufbauen, kann der Fehler „untergehen“. Deshalb ist es in der asynchronen Programmierung sehr wichtig, Fehler direkt in den CompletableFuture-Ketten abzufangen und zu verarbeiten.

2. Methode exceptionally: Fehlerbehandlung und Rückgabewert

Die Methode exceptionally ermöglicht es, eine Ausnahme aus vorherigen Schritten der Kette abzufangen, sie zu verarbeiten und einen alternativen Wert zurückzugeben. Das ist wie catch, nur für einen asynchronen Datenstrom.

Signatur:

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

Beispiel

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("Wir führen eine riskante Berechnung aus...");
    if (Math.random() > 0.5) {
        throw new RuntimeException("Etwas ist schiefgelaufen!");
    }
    return 42;
});

future = future.exceptionally(ex -> {
    System.out.println("Es ist ein Fehler aufgetreten: " + ex.getMessage());
    return 0; // Wir geben einen "sicheren" Wert zurück
});

Beispiel mit thenAccept

future.thenAccept(result -> System.out.println("Ergebnis: " + result));

Ausgabe (beispielhaft):

Wir führen eine riskante Berechnung aus...
Es ist ein Fehler aufgetreten: Etwas ist schiefgelaufen!
Ergebnis: 0
Wir führen eine riskante Berechnung aus...
Ergebnis: 42

Wichtig! Die Methode exceptionally wird nur ausgelöst, wenn zuvor in der Kette eine unbehandelte Ausnahme aufgetreten ist. Wenn alles gut läuft, „reicht“ sie das Ergebnis einfach weiter.

3. Methode handle: universeller Handler für Ergebnis und Fehler

Manchmal möchten wir sowohl das Ergebnis als auch den Fehler gemeinsam bearbeiten. Zum Beispiel: Wenn alles gut ist – das Ergebnis zurückgeben; wenn ein Fehler auftritt – einen Ersatzwert liefern oder den Fehler protokollieren.

Signatur:

CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
  • Erstes Argument – das Ergebnis (oder null, wenn ein Fehler auftrat),
  • Zweites – die Ausnahme (oder null, wenn alles gut ist).

Beispiel

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Zufälliger Fehler!");
    return 100;
});

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

safeFuture.thenAccept(r -> System.out.println("Endergebnis: " + r));

Ausgabe:

Fehler erkannt: Zufälliger Fehler!
Endergebnis: -1
Endergebnis: 100

handle sollten Sie verwenden, wenn Sie unabhängig vom Ausgang der Aufgabe handeln wollen – egal ob erfolgreich oder mit Fehler. Es ist ein universeller Abschluss-Handler, der immer aufgerufen wird und zwei Argumente erhält: das Ergebnis (wenn alles gut ist) und die Ausnahme (wenn etwas schiefging).

Die Methode eignet sich ideal, um Fehler zentral zu loggen, einen Standardwert zurückzugeben, ohne die Kette abzubrechen, oder ein asynchrones Szenario sauber zu beenden.

Beispiel:

CompletableFuture<Integer> future = CompletableFuture
    .supplyAsync(() -> 10 / 0) // hier passiert ein Fehler
    .handle((result, ex) -> {
        if (ex != null) {
            System.out.println("Fehler: " + ex.getMessage());
            return 0; // Standardwert
        }
        return result;
    });

System.out.println(future.join()); // gibt 0 aus

Im Unterschied zu exceptionally, das nur auf Fehler reagiert, wird handle immer ausgeführt. So können Sie beide Ausgänge an einem Ort behandeln und die Kette flüssig halten.

4. Methode whenComplete: Nebenaktionen nach Abschluss

Manchmal müssen wir das Ergebnis nicht verändern, sondern nur eine Aktion nach Abschluss der Aufgabe ausführen – z. B. protokollieren, dass die Aufgabe abgeschlossen wurde, unabhängig davon, ob erfolgreich oder mit Fehler.

Signatur:

CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
  • Erstes Argument – das Ergebnis (oder null bei Fehler),
  • Zweites – die Ausnahme (oder null bei Erfolg).

Beispiel

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

future.whenComplete((result, ex) -> {
    if (ex != null) {
        System.out.println("Fehler bei der Ausführung: " + ex.getMessage());
    } else {
        System.out.println("Erfolgreich abgeschlossen, Ergebnis: " + result);
    }
});

Wichtiger Unterschied:
whenComplete ändert weder Ergebnis noch Fehler, sondern führt nur eine Aktion aus. Wenn in whenComplete eine Ausnahme auftritt, wird sie an die bereits vorhandene „angefügt“.

Beispiel: Wir loggen, greifen aber nicht ein

future
    .whenComplete((res, ex) -> {
        System.out.println("Aufgabe abgeschlossen. Fehler? " + (ex != null));
    })
    .thenAccept(r -> System.out.println("Ergebnis für den Benutzer: " + r));

5. Besonderheiten und Feinheiten der Implementierung

Best Practices: Fehler in CompletableFuture richtig behandeln

  • Immer Fehlerbehandlung (exceptionally, handle oder whenComplete) in asynchrone Ketten einbauen. Andernfalls kann ein Fehler unbemerkt bleiben, und die Anwendung verhält sich unvorhersehbar.
  • Verwenden Sie get() oder join() nicht im Hauptthread ohne try-catch – das macht asynchronen Code synchron und kann zu Blockierungen führen.
  • Wenn Sie bei einem Fehler einen Fallback-Wert zurückgeben müssen – verwenden Sie exceptionally oder handle.
  • Für Nebenwirkungen (Logging, Benachrichtigung des Nutzers) – verwenden Sie whenComplete.
  • In Ketten können Sie kombinieren: z. B. zuerst den Fehler mit exceptionally behandeln, dann mit whenComplete loggen und anschließend die Ergebnisverarbeitung fortsetzen.
  • Denken Sie daran: Wenn der Fehler nicht behandelt wird, „sickert“ er in den nächsten Aufruf von get()/join() und kann die Anwendung zum Absturz bringen.

Reihenfolge der Methoden

  • Wenn Sie exceptionally verwenden, fängt es nur Fehler ab, die zuvor in der Kette aufgetreten sind.
  • Wenn nach exceptionally erneut ein Fehler in der Kette auftritt (z. B. in thenApply), müssen Sie ihn separat behandeln.
  • handle ist universell – es wird immer ausgeführt, unabhängig davon, ob ein Fehler aufgetreten ist oder nicht.

Kombinieren von Methoden

CompletableFuture.supplyAsync(() -> {
    // ...
})
.handle((result, ex) -> {
    if (ex != null) return "Fehler: " + ex.getMessage();
    return result;
})
.whenComplete((res, ex) -> {
    System.out.println("Die Aufgabe wurde beendet, Ergebnis: " + res);
});

Was passiert, wenn der Fehler nicht behandelt wird?

Wenn die Ausnahme nicht behandelt wird und Sie get() oder join() aufrufen, wird sie als ExecutionException (oder CompletionException) ausgelöst, und die Anwendung kann mit einem Fehler beendet werden.

6. Häufige Fehler bei der Fehlerbehandlung in CompletableFuture

Fehler Nr. 1: fehlende Fehlerbehandlung. Wenn weder exceptionally noch handle noch whenComplete hinzugefügt wird, „geht“ der Fehler bis zum nächsten Aufruf von get()/join() verloren, der weit vom Ort des Auftretens entfernt sein kann.

Fehler Nr. 2: Verwendung von get()/join() im Hauptthread ohne try-catch. Das macht asynchronen Code synchron und kann zu Blockierungen oder unerwarteten Abstürzen der Anwendung führen.

Fehler Nr. 3: falsches Verständnis, wo genau der Handler ausgeführt wird. exceptionally fängt nur Fehler ab, die vor ihm in der Kette aufgetreten sind. Wenn danach erneut ein Fehler auftritt, wird er von dieser Methode nicht behandelt.

Fehler Nr. 4: Fehler wird behandelt, aber ohne einen Wert zurückzugeben. In exceptionally oder handle muss unbedingt ein Wert zurückgegeben werden, sonst erhält der nächste Schritt der Kette null (oder erhält gar nichts).

Fehler Nr. 5: Verwechslung zwischen handle und whenComplete. handle kann das Ergebnis ändern, whenComplete führt nur eine Aktion aus (z. B. Logging). Wenn Sie das Ergebnis ändern möchten – verwenden Sie handle.

Fehler Nr. 6: doppelte Logik der Fehlerbehandlung. Oft lässt sich die Fehlerbehandlung an einer Stelle bündeln, um Code-Duplizierung zu vermeiden – z. B. über ein zentrales handle oder einen gemeinsamen Handler.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION