1. Problem: wyjątki w kodzie asynchronicznym
W zwykłym (synchronicznym) kodzie wszystko jest proste: jeśli w metodzie wystąpi wyjątek, propaguje się on w górę stosu wywołań i możemy go złapać za pomocą try-catch. Na przykład:
try {
int x = 1 / 0;
} catch (ArithmeticException ex) {
System.out.println("Dzielenie przez zero!");
}
W kodzie asynchronicznym sytuacja jest trudniejsza. Gdy uruchamiamy zadanie przez CompletableFuture.supplyAsync, wykonuje się ono w innym wątku. Jeśli tam wystąpi wyjątek, nie zostanie on wyrzucony w wątku głównym! Zamiast tego zostanie „zapakowany” wewnątrz obiektu CompletableFuture, i jeśli później wywołasz get() lub join(), otrzymasz ten wyjątek w postaci ExecutionException.
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// Ups, tutaj błąd!
return 1 / 0;
});
try {
Integer result = future.get(); // tutaj zostanie rzucony wyjątek!
} catch (Exception ex) {
System.out.println("Wystąpił błąd: " + ex.getMessage());
}
Ale jeśli nie wywołujesz get() (co, nawiasem mówiąc, samo w sobie nie jest zbyt asynchroniczne), a budujesz łańcuchy przez thenApply i inne metody, błąd może się zagubić. Dlatego w programowaniu asynchronicznym bardzo ważne jest umieć łapać i obsługiwać błędy właśnie w łańcuchach CompletableFuture.
2. Metoda exceptionally: obsługa błędów i zwrot wartości
Metoda exceptionally pozwala złapać wyjątek, jeśli wystąpił na wcześniejszych etapach łańcucha, obsłużyć go i zwrócić alternatywną wartość. To jak catch, tylko dla asynchronicznego strumienia danych.
Sygnatura:
CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
Przykład użycia
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Wykonujemy niebezpieczne obliczenia...");
if (Math.random() > 0.5) {
throw new RuntimeException("Coś poszło nie tak!");
}
return 42;
});
future = future.exceptionally(ex -> {
System.out.println("Wystąpił błąd: " + ex.getMessage());
return 0; // Zwracamy 'bezpieczną' wartość
});
Przykład z thenAccept
future.thenAccept(result -> System.out.println("Wynik: " + result));
Wyjście (przykładowe):
Wykonujemy niebezpieczne obliczenia...
Wystąpił błąd: Coś poszło nie tak!
Wynik: 0
Wykonujemy niebezpieczne obliczenia...
Wynik: 42
Ważne! Metoda exceptionally działa tylko wtedy, gdy wcześniej w łańcuchu wystąpił nieobsłużony wyjątek. Jeśli wszystko poszło dobrze, po prostu przepuszcza wynik dalej.
3. Metoda handle: uniwersalny handler wyniku i błędu
Czasem musimy obsłużyć zarówno wynik, jak i błąd jednocześnie. Na przykład: jeśli wszystko jest dobrze — zwrócić wynik; jeśli jest błąd — zwrócić wariant awaryjny lub zalogować błąd.
Sygnatura:
CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
- Pierwszy argument — wynik (albo null, jeśli był błąd),
- Drugi — wyjątek (albo null, jeśli wszystko jest dobrze).
Przykład użycia
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("Losowy błąd!");
return 100;
});
CompletableFuture<Integer> safeFuture = future.handle((result, ex) -> {
if (ex != null) {
System.out.println("Wykryto błąd: " + ex.getMessage());
return -1;
}
return result;
});
safeFuture.thenAccept(r -> System.out.println("Wynik końcowy: " + r));
Wyjście:
Wykryto błąd: Losowy błąd!
Wynik końcowy: -1
Wynik końcowy: 100
handle warto używać wtedy, gdy chcesz działać niezależnie od tego, jak zakończyło się zadanie — sukcesem czy błędem. To uniwersalny handler wyników, który zawsze jest wywoływany i otrzymuje dwa argumenty: wynik (jeśli wszystko jest dobrze) oraz wyjątek (jeśli coś poszło nie tak).
Metoda idealnie nadaje się, gdy trzeba centralnie logować błędy, zwrócić wartość domyślną bez przerywania łańcucha albo po prostu elegancko domknąć scenariusz asynchroniczny.
Przykład:
CompletableFuture<Integer> future = CompletableFuture
.supplyAsync(() -> 10 / 0) // tutaj wystąpi błąd
.handle((result, ex) -> {
if (ex != null) {
System.out.println("Błąd: " + ex.getMessage());
return 0; // wartość domyślna
}
return result;
});
System.out.println(future.join()); // wypisze 0
W przeciwieństwie do exceptionally, który reaguje jedynie na błędy, handle wywołuje się zawsze, pozwalając obsłużyć oba scenariusze w jednym miejscu i zachować płynność całego łańcucha.
4. Metoda whenComplete: poboczne działania po zakończeniu
Czasem nie chcemy zmieniać wyniku, a jedynie wykonać jakieś działanie po zakończeniu zadania — na przykład zalogować, że zadanie się zakończyło, niezależnie od tego, czy sukcesem, czy błędem.
Sygnatura:
CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
- Pierwszy argument — wynik (albo null przy błędzie),
- Drugi — wyjątek (albo null przy sukcesie).
Przykład użycia
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("Błąd!");
return 10;
});
future.whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("Błąd podczas wykonania: " + ex.getMessage());
} else {
System.out.println("Zakończono pomyślnie, wynik: " + result);
}
});
Ważna różnica:
whenComplete nie zmienia wyniku ani błędu, a jedynie wykonuje działanie. Jeśli w whenComplete wystąpi wyjątek, zostanie on dołączony do już istniejącego.
Przykład: logujemy, ale nie ingerujemy
future
.whenComplete((res, ex) -> {
System.out.println("Zadanie zakończone. Błąd? " + (ex != null));
})
.thenAccept(r -> System.out.println("Wynik dla użytkownika: " + r));
5. Specyfika i niuanse implementacyjne
Best practices: jak prawidłowo obsługiwać błędy w CompletableFuture
- Zawsze dodawaj obsługę błędów (exceptionally, handle lub whenComplete) w łańcuchach zadań asynchronicznych. W przeciwnym razie błąd może pozostać niezauważony, a aplikacja będzie zachowywać się nieprzewidywalnie.
- Nie używaj get() ani join() w wątku głównym bez try-catch — to zamieni kod asynchroniczny w synchroniczny i może prowadzić do blokad.
- Jeśli trzeba zwrócić „zapasową” wartość przy błędzie — użyj exceptionally lub handle.
- Do efektów ubocznych (logowanie, powiadomienie użytkownika) — użyj whenComplete.
- W łańcuchach można łączyć: np. najpierw obsłużyć błąd przez exceptionally, potem zalogować przez whenComplete, a następnie kontynuować przetwarzanie wyniku.
- Pamiętaj, że jeśli błąd nie zostanie obsłużony, przeniesie się do kolejnego wywołania get()/join() i może doprowadzić do awarii aplikacji.
Kolejność metod
- Jeśli używasz exceptionally, przechwytuje on tylko błędy, które wystąpiły przed nim w łańcuchu.
- Jeśli po exceptionally w łańcuchu znowu wystąpi błąd (np. w thenApply), trzeba go obsłużyć osobno.
- handle jest uniwersalny — zawsze się wywołuje, niezależnie od tego, czy był błąd, czy nie.
Łączenie metod
CompletableFuture.supplyAsync(() -> {
// ...
})
.handle((result, ex) -> {
if (ex != null) return "Błąd: " + ex.getMessage();
return result;
})
.whenComplete((res, ex) -> {
System.out.println("Zadanie zakończyło się, wynik: " + res);
});
Co, jeśli nie obsłużysz błędu?
Jeśli wyjątek nie jest obsłużony i wywołasz get() lub join(), zostanie on rzucony jako ExecutionException (lub CompletionException), a aplikacja może zakończyć się błędem.
6. Typowe błędy podczas obsługi błędów w CompletableFuture
Błąd nr 1: brak obsługi błędów. Jeśli nie dodasz ani exceptionally, ani handle, ani whenComplete, błąd po prostu „zginie” do następnego wywołania get()/join(), które może być daleko od miejsca jego powstania.
Błąd nr 2: używanie get()/join() w wątku głównym bez try-catch. To zamienia kod asynchroniczny w synchroniczny i może prowadzić do blokad lub nieoczekiwanych awarii aplikacji.
Błąd nr 3: błędne rozumienie, gdzie dokładnie uruchamia się obsługa. exceptionally łapie tylko błędy przed sobą w łańcuchu. Jeśli po nim znowu wystąpi błąd, ten metodą nie zostanie obsłużony.
Błąd nr 4: obsługa błędu, ale bez zwrotu wartości. W metodzie exceptionally lub handle koniecznie trzeba zwrócić wartość, w przeciwnym razie następny etap łańcucha dostanie null (albo nie dostanie niczego).
Błąd nr 5: mylenie handle i whenComplete. handle może zmieniać wynik, a whenComplete — tylko wykonywać działanie (np. logowanie). Jeśli chcesz zmienić wynik — użyj handle.
Błąd nr 6: duplikowanie logiki obsługi błędów. Często można połączyć obsługę błędów w jednym miejscu, aby uniknąć duplikacji kodu — na przykład przez scentralizowany handle lub wspólny handler.
GO TO FULL VERSION