1. Il problema: eccezioni nel codice asincrono
Nel codice normale (sincrono) è tutto semplice: se in un metodo si verifica un'eccezione, essa “risale” lo stack delle chiamate e possiamo intercettarla con try-catch. Per esempio:
try {
int x = 1 / 0;
} catch (ArithmeticException ex) {
System.out.println("Divisione per zero!");
}
Nel codice asincrono la situazione è più complessa. Quando avviamo un task tramite CompletableFuture.supplyAsync, esso viene eseguito in un altro thread. Se lì si verifica un'eccezione, non verrà lanciata nel thread principale! Invece verrà “incapsulata” dentro l'oggetto CompletableFuture e, se poi chiamate get() o join(), riceverete tale eccezione sotto forma di ExecutionException.
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// Ops, qui c'è un errore!
return 1 / 0;
});
try {
Integer result = future.get(); // qui verrà lanciata un'eccezione!
} catch (Exception ex) {
System.out.println("Si è verificato un errore: " + ex.getMessage());
}
Ma se non chiamate get() (il che, tra l'altro, non è molto asincrono di per sé) e costruite invece catene tramite thenApply e altri metodi, l'errore può “perdersi”. Perciò nella programmazione asincrona è molto importante saper intercettare e gestire gli errori direttamente nelle catene di CompletableFuture.
2. Il metodo exceptionally: gestione degli errori e valore di ritorno
Il metodo exceptionally consente di intercettare un'eccezione se si è verificata nelle fasi precedenti della catena, gestirla e restituire un valore alternativo. È come un catch, ma per il flusso di dati asincrono.
Firma:
CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
Esempio di utilizzo
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Eseguiamo un calcolo rischioso...");
if (Math.random() > 0.5) {
throw new RuntimeException("Qualcosa è andato storto!");
}
return 42;
});
future = future.exceptionally(ex -> {
System.out.println("Si è verificato un errore: " + ex.getMessage());
return 0; // Restituiamo un valore “sicuro”
});
Esempio con thenAccept
future.thenAccept(result -> System.out.println("Risultato: " + result));
Output (esempio):
Eseguiamo un calcolo rischioso...
Si è verificato un errore: Qualcosa è andato storto!
Risultato: 0
Eseguiamo un calcolo rischioso...
Risultato: 42
Importante! Il metodo exceptionally viene eseguito soltanto se nella catena prima di esso si è verificata un'eccezione non gestita. Se tutto va bene, semplicemente “propaga” il risultato oltre.
3. Metodo handle: gestore universale di risultato ed errore
Talvolta è necessario gestire sia il risultato, sia l'errore contemporaneamente. Per esempio, se va tutto bene — restituire il risultato; in caso di errore — restituire un'alternativa o registrare l'errore nei log.
Firma:
CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
- Primo argomento — il risultato (oppure null se c'è stato un errore),
- Secondo — l'eccezione (oppure null se è andato tutto bene).
Esempio di utilizzo
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("Errore casuale!");
return 100;
});
CompletableFuture<Integer> safeFuture = future.handle((result, ex) -> {
if (ex != null) {
System.out.println("Rilevato errore: " + ex.getMessage());
return -1;
}
return result;
});
safeFuture.thenAccept(r -> System.out.println("Risultato finale: " + r));
Output:
Rilevato errore: Errore casuale!
Risultato finale: -1
Risultato finale: 100
handle conviene usarlo quando desiderate agire indipendentemente dall'esito del task — con successo o con errore. È un gestore universale degli esiti, che viene chiamato sempre e riceve due argomenti: il risultato (se tutto va bene) e l'eccezione (se qualcosa è andato storto).
Il metodo è ideale per registrare gli errori in modo centralizzato, restituire un valore di default senza interrompere la catena oppure semplicemente concludere in modo pulito lo scenario asincrono.
Esempio:
CompletableFuture<Integer> future = CompletableFuture
.supplyAsync(() -> 10 / 0) // qui si verificherà un errore
.handle((result, ex) -> {
if (ex != null) {
System.out.println("Errore: " + ex.getMessage());
return 0; // valore predefinito
}
return result;
});
System.out.println(future.join()); // stamperà 0
A differenza di exceptionally, che reagisce solo agli errori, handle viene eseguito sempre, consentendo di gestire entrambi gli esiti in un unico punto e mantenere la fluidità dell'intera catena.
4. Metodo whenComplete: effetti collaterali dopo il completamento
A volte non è necessario modificare il risultato, ma semplicemente eseguire un'azione al termine del task — ad esempio registrare nei log che il task è terminato, indipendentemente dal fatto che sia andato a buon fine o con errore.
Firma:
CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
- Primo argomento — il risultato (oppure null in caso di errore),
- Secondo — l'eccezione (oppure null in caso di successo).
Esempio di utilizzo
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("Errore!");
return 10;
});
future.whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("Errore durante l'esecuzione: " + ex.getMessage());
} else {
System.out.println("Completato con successo, risultato: " + result);
}
});
Differenza importante:
whenComplete non modifica il risultato o l'errore, ma esegue soltanto un'azione. Se in whenComplete si verifica un'eccezione, essa verrà “aggregata” a quella già esistente.
Esempio: facciamo logging senza intervenire
future
.whenComplete((res, ex) -> {
System.out.println("Task completato. Errore? " + (ex != null));
})
.thenAccept(r -> System.out.println("Risultato per l'utente: " + r));
5. Dettagli e sfumature di implementazione
Best practice: come gestire correttamente gli errori in CompletableFuture
- Sempre aggiungete la gestione degli errori (exceptionally, handle o whenComplete) nelle catene di task asincroni. In caso contrario, un errore potrebbe passare inosservato e l'applicazione potrebbe comportarsi in modo imprevedibile.
- Non utilizzate get() o join() nel thread principale senza try-catch — ciò trasforma il codice asincrono in sincrono e può portare a blocchi.
- Se occorre restituire un valore di “fallback” in caso di errore — utilizzate exceptionally o handle.
- Per effetti collaterali (logging, notifica all'utente) — utilizzate whenComplete.
- Nelle catene si possono combinare: ad esempio, prima gestire l'errore tramite exceptionally, poi registrare con whenComplete, quindi proseguire l'elaborazione del risultato.
- Ricordate che, se l'errore non è gestito, “propagherà” alla successiva chiamata get()/join() e può causare il crash dell'applicazione.
Ordine dei metodi
- Se usate exceptionally, intercetta solo gli errori avvenuti prima di esso nella catena.
- Se dopo exceptionally nella catena si verifica nuovamente un errore (ad esempio in thenApply), va gestito separatamente.
- handle è universale — viene sempre eseguito, a prescindere dal fatto che ci sia stato un errore o meno.
Combinazione dei metodi
CompletableFuture.supplyAsync(() -> {
// ...
})
.handle((result, ex) -> {
if (ex != null) return "Errore: " + ex.getMessage();
return result;
})
.whenComplete((res, ex) -> {
System.out.println("Il task si è concluso, risultato: " + res);
});
Cosa succede se non si gestisce l'errore?
Se un'eccezione non viene gestita e chiamate get() o join(), verrà lanciata come ExecutionException (o CompletionException) e l'applicazione potrebbe terminare con un errore.
6. Errori tipici nella gestione degli errori in CompletableFuture
Errore n. 1: assenza di gestione degli errori. Se non aggiungete né exceptionally, né handle, né whenComplete, l'errore semplicemente “si perderà” fino alla successiva chiamata get()/join(), che potrebbe essere lontana dal punto in cui è stato generato.
Errore n. 2: uso di get()/join() nel thread principale senza try-catch. Questo rende il codice asincrono sincrono e può portare a blocchi o a crash inaspettati dell'applicazione.
Errore n. 3: comprensione errata di dove esattamente viene eseguito il gestore. exceptionally intercetta solo gli errori precedenti nella catena. Se dopo di esso si verifica di nuovo un errore, non verrà gestito da questo metodo.
Errore n. 4: gestire l'errore ma senza restituire un valore. Nei metodi exceptionally o handle è obbligatorio restituire un valore, altrimenti la fase successiva riceverà null (o non riceverà nulla).
Errore n. 5: confusione tra handle e whenComplete. handle può modificare il risultato, mentre whenComplete esegue soltanto un'azione (ad esempio il logging). Se volete modificare il risultato — usate handle.
Errore n. 6: duplicazione della logica di gestione degli errori. Spesso è possibile riunire la gestione degli errori in un unico punto per evitare duplicazioni di codice — ad esempio tramite un handle centralizzato o un gestore comune.
GO TO FULL VERSION