CompletableFuture: Tratamento de Erros e Finalização de Tarefas Assíncronas em Java
O CompletableFuture permite executar tarefas assíncronas e reagir aos seus resultados por meio de callbacks.
Mas o que acontece quando ocorre uma exceção durante a execução da tarefa?
O Java fornece uma API completa no CompletableFuture para tratar esses casos e garantir que a aplicação continue funcionando corretamente, mesmo em situações de erro.
Tratamento de erros com exceptionally()
Quando uma exceção ocorre dentro de uma tarefa assíncrona, ela é encapsulada em um objeto CompletionException.
Para capturar e lidar com esse erro, o CompletableFuture oferece os métodos exceptionally() e exceptionallyCompose():
CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
CompletableFuture<T> exceptionallyCompose(Function<Throwable, ? extends CompletionStage<T>> fn)exceptionally()permite processar o erro e retornar um valor alternativo.exceptionallyCompose()também processa o erro, mas retorna outroCompletableFuture, o que permite continuar a execução de forma assíncrona.
Exemplo: tratamento de exceção no cálculo de fatorial
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.function.Function;
import java.util.function.Consumer;
class Program {
public static void main(String[] args) throws Exception {
System.out.println("Main thread started...");
int number = -5; // número inválido para o fatorial
Supplier<Integer> factorialTask = () -> {
if (number < 1) throw new RuntimeException("Number must be greater than 0");
int n = 1;
int result = 1;
while (n <= number) result *= n++;
return result;
};
Consumer<Integer> printTask = result ->
System.out.printf("Final result: %d\n", result);
Function<Throwable, Integer> printException = ex -> {
System.out.println(ex.getMessage());
throw new RuntimeException(ex.getMessage());
};
ExecutorService executor = Executors.newCachedThreadPool();
CompletableFuture
.supplyAsync(factorialTask, executor)
.exceptionally(printException)
.thenAccept(printTask);
System.out.println("Main thread works...");
System.out.println("Main thread finished...");
executor.close();
}
}Saída esperada:
Main thread started... java.lang.RuntimeException: Number must be greater than 0 Main thread works... Main thread finished...
Como o número é inválido, a tarefa lança uma exceção.
O método exceptionally() captura essa exceção e executa a função printException, que imprime a mensagem.
O callback seguinte (thenAccept) não é executado, pois a exceção foi propagada novamente.
Se o número fosse válido (int number = 5;), a saída seria:
Main thread started... Final result: 120 Main thread works... Main thread finished...
Retornando um valor alternativo
Em vez de relançar a exceção, também é possível retornar um valor padrão dentro de exceptionally(), garantindo que a cadeia continue sua execução:
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.function.Function;
import java.util.function.Consumer;
class Program {
public static void main(String[] args) throws Exception {
int age = -5;
Supplier<Integer> validateAge = () -> {
if (age > 110 || age < 1) throw new RuntimeException("Age is invalid");
return age;
};
Consumer<Integer> printAge = result ->
System.out.printf("Final age: %d\n", result);
Function<Throwable, Integer> printException = ex -> {
System.out.println(ex.getMessage());
return 18; // valor padrão
};
ExecutorService executor = Executors.newCachedThreadPool();
CompletableFuture
.supplyAsync(validateAge, executor)
.exceptionally(printException)
.thenAccept(printAge);
executor.close();
}
}Saída esperada:
java.lang.RuntimeException: Age is invalid Final age: 18
Neste caso, o erro é capturado, uma mensagem é exibida e o fluxo segue normalmente com o valor padrão 18.
Exceção CompletionException
Quando uma exceção ocorre dentro de um CompletableFuture, ela é envolvida em uma CompletionException.
Por isso, a função de tratamento em exceptionally() recebe uma exceção cujo tipo é CompletionException, e não o tipo original.
Para acessar o erro original, pode-se usar o método getCause():
Function<Throwable, Integer> printException = ex -> {
System.out.println(ex.getCause().getMessage());
return 18;
};Saída:
Age is invalid Final age: 18
Tratando conclusão com whenComplete()
Nem sempre é necessário tratar apenas erros.
Às vezes, é desejável executar uma ação ao final da tarefa — seja em caso de sucesso ou falha.
O método whenComplete() serve exatamente para isso:
CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)O BiConsumer passado ao método recebe dois argumentos:
- o resultado da tarefa, se bem-sucedida,
- a exceção, se algo deu errado.
Quando não há erro, o segundo parâmetro é null; quando há falha, o primeiro é null.
Exemplo com whenComplete()
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.function.BiConsumer;
class Program {
public static void main(String[] args) throws Exception {
System.out.println("Main thread started...");
ExecutorService executor = Executors.newCachedThreadPool();
work(executor, 5);
work(executor, -5);
System.out.println("Main thread finished...");
executor.close();
}
static void work(Executor executor, int number) {
Supplier<Integer> factorialTask = () -> {
if (number < 1)
throw new RuntimeException("Number must be greater than 0. Invalid number: " + number);
int n = 1;
int result = 1;
while (n <= number) result *= n++;
return result;
};
BiConsumer<Integer, Throwable> completedTask = (result, ex) -> {
if (ex == null)
System.out.printf("Factorial of %d is %d\n", number, result);
else
System.out.println(ex.getCause().getMessage());
};
CompletableFuture
.supplyAsync(factorialTask, executor)
.whenComplete(completedTask);
}
}Saída típica (ordem não determinística):
Main thread started... Factorial of 5 is 120 Number must be greater than 0. Invalid number: -5 Main thread finished...
Nesse exemplo, a função completedTask é chamada tanto no sucesso quanto no erro.
O parâmetro result contém o valor do fatorial quando não há falha, e o parâmetro ex contém a exceção quando algo dá errado.
Resumo
exceptionally()trata erros e pode retornar um valor padrão.exceptionallyCompose()permite continuar o fluxo de forma assíncrona após um erro.CompletionExceptionencapsula o erro original; o métodogetCause()recupera a exceção real.whenComplete()executa uma ação ao final da tarefa, seja com sucesso ou com falha.- Essas ferramentas tornam o
CompletableFutureideal para cenários complexos de execução assíncrona com controle refinado de erros.