1. O problema: exceções em código assíncrono
No código comum (síncrono) é simples: se uma exceção ocorre em um método, ela é propagada para cima pela pilha de chamadas e podemos capturá-la com try-catch. Por exemplo:
try {
int x = 1 / 0;
} catch (ArithmeticException ex) {
System.out.println("Divisão por zero!");
}
No código assíncrono a situação é mais complexa. Quando iniciamos uma tarefa via CompletableFuture.supplyAsync, ela é executada em outra thread. Se ocorrer uma exceção lá, ela não será lançada na thread principal! Em vez disso, ela é “empacotada” dentro do objeto CompletableFuture e, se você depois chamar get() ou join(), receberá essa exceção na forma de ExecutionException.
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// Ops, ocorreu um erro aqui!
return 1 / 0;
});
try {
Integer result = future.get(); // aqui uma exceção será lançada!
} catch (Exception ex) {
System.out.println("Ocorreu um erro: " + ex.getMessage());
}
Mas se você não chama get() (o que, aliás, por si só não é muito assíncrono) e constrói cadeias usando thenApply e outros métodos, o erro pode “se perder”. Por isso, em programação assíncrona é muito importante saber capturar e tratar erros diretamente nas cadeias de CompletableFuture.
2. Método exceptionally: tratar erros e retornar um valor
O método exceptionally permite capturar a exceção, caso ela tenha ocorrido nas etapas anteriores da cadeia, tratá-la e retornar um valor alternativo. É como um catch, só que para o fluxo de dados assíncrono.
Assinatura:
CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
Exemplo de uso
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Executando um cálculo arriscado...");
if (Math.random() > 0.5) {
throw new RuntimeException("Algo deu errado!");
}
return 42;
});
future = future.exceptionally(ex -> {
System.out.println("Ocorreu um erro: " + ex.getMessage());
return 0; // Retornamos um valor "seguro"
});
Exemplo com thenAccept
future.thenAccept(result -> System.out.println("Resultado: " + result));
Saída (aproximada):
Executando um cálculo arriscado...
Ocorreu um erro: Algo deu errado!
Resultado: 0
Executando um cálculo arriscado...
Resultado: 42
Importante! O método exceptionally é acionado somente se ocorrer uma exceção não tratada anteriormente na cadeia. Se tudo correr bem, ele simplesmente “deixa” o resultado passar adiante.
3. Método handle: manipulador universal de resultado e erro
Às vezes precisamos tratar tanto o resultado quanto o erro ao mesmo tempo. Por exemplo: se tudo correu bem — retornamos o resultado; se houve erro — retornamos uma opção de fallback ou registramos o erro no log.
Assinatura:
CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
- O primeiro argumento — o resultado (ou null, se houve erro),
- O segundo — a exceção (or null, se tudo correu bem).
Exemplo de uso
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("Erro aleatório!");
return 100;
});
CompletableFuture<Integer> safeFuture = future.handle((result, ex) -> {
if (ex != null) {
System.out.println("Erro detectado: " + ex.getMessage());
return -1;
}
return result;
});
safeFuture.thenAccept(r -> System.out.println("Resultado final: " + r));
Saída:
Erro detectado: Erro aleatório!
Resultado final: -1
Resultado final: 100
Use handle quando você deseja agir independentemente de como a tarefa terminou — com sucesso ou com erro. É um manipulador universal de resultados que sempre é chamado e recebe dois argumentos: o resultado (se tudo correu bem) e a exceção (se algo deu errado).
O método é ideal se você precisa centralizar o log de erros, retornar um valor padrão sem quebrar a cadeia, ou simplesmente finalizar com cuidado o cenário assíncrono.
Exemplo:
CompletableFuture<Integer> future = CompletableFuture
.supplyAsync(() -> 10 / 0) // aqui ocorrerá um erro
.handle((result, ex) -> {
if (ex != null) {
System.out.println("Erro: " + ex.getMessage());
return 0; // valor padrão
}
return result;
});
System.out.println(future.join()); // imprimirá 0
Ao contrário de exceptionally, que reage apenas a erros, handle é acionado sempre, permitindo tratar ambos os desfechos em um só lugar e manter a fluidez de toda a cadeia.
4. Método whenComplete: ações colaterais após a conclusão
Às vezes não precisamos mudar o resultado, apenas executar alguma ação após a conclusão da tarefa — por exemplo, registrar no log que a tarefa terminou, independentemente de ter sido com sucesso ou com erro.
Assinatura:
CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
- O primeiro argumento — o resultado (ou null em caso de erro),
- O segundo — a exceção (ou null em caso de sucesso).
Exemplo de uso
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("Erro!");
return 10;
});
future.whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("Erro durante a execução: " + ex.getMessage());
} else {
System.out.println("Concluído com sucesso, resultado: " + result);
}
});
Diferença importante:
whenComplete não altera o resultado ou o erro — ele apenas executa uma ação. Se ocorrer uma exceção dentro de whenComplete, ela será “agregada” à já existente.
Exemplo: registramos no log, mas não interferimos
future
.whenComplete((res, ex) -> {
System.out.println("Tarefa concluída. Erro? " + (ex != null));
})
.thenAccept(r -> System.out.println("Resultado para o usuário: " + r));
5. Particularidades e nuances de implementação
Boas práticas: como tratar erros corretamente no CompletableFuture
- Sempre adicione tratamento de erros (exceptionally, handle ou whenComplete) às cadeias de tarefas assíncronas. Caso contrário, o erro pode passar despercebido e o aplicativo se comportará de maneira imprevisível.
- Não use get() ou join() na thread principal sem try-catch — isso transforma código assíncrono em síncrono e pode levar a bloqueios.
- Se precisar retornar um valor “de fallback” em caso de erro — use exceptionally ou handle.
- Para efeitos colaterais (log, notificação do usuário) — use whenComplete.
- É possível combinar na cadeia: por exemplo, primeiro tratar o erro com exceptionally, depois registrar no log com whenComplete e então continuar o processamento do resultado.
- Lembre-se de que, se o erro não for tratado, ele “vai parar” na próxima chamada de get()/join() e pode derrubar o aplicativo.
Ordem dos métodos
- Se você usar exceptionally, ele intercepta apenas os erros que ocorreram antes dele na cadeia.
- Se após exceptionally ocorrer outro erro na cadeia (por exemplo, em thenApply), ele precisa ser tratado separadamente.
- handle é universal — ele é sempre acionado, independentemente de ter havido erro ou não.
Combinando métodos
CompletableFuture.supplyAsync(() -> {
// ...
})
.handle((result, ex) -> {
if (ex != null) return "Erro: " + ex.getMessage();
return result;
})
.whenComplete((res, ex) -> {
System.out.println("A tarefa foi concluída, resultado: " + res);
});
O que acontece se você não tratar o erro?
Se a exceção não for tratada e você chamar get() ou join(), ela será lançada como ExecutionException (ou CompletionException) e o aplicativo pode encerrar com erro.
6. Erros típicos ao tratar erros no CompletableFuture
Erro nº 1: ausência de tratamento de erros. Se você não adicionar exceptionally, handle ou whenComplete, o erro simplesmente “se perderá” até a próxima chamada de get()/join(), que pode estar distante do ponto de origem.
Erro nº 2: usar get()/join() na thread principal sem try-catch. Isso torna o código assíncrono em síncrono e pode causar bloqueios ou encerramentos inesperados do aplicativo.
Erro nº 3: entender incorretamente onde exatamente o manipulador é acionado. exceptionally captura apenas erros anteriores a ele na cadeia. Se depois dele ocorrer novamente um erro, esse método não o tratará.
Erro nº 4: tratar o erro, mas sem retornar um valor. Nos métodos exceptionally ou handle é obrigatório retornar um valor, caso contrário a próxima etapa da cadeia receberá null (ou não receberá nada).
Erro nº 5: confundir handle com whenComplete. handle pode alterar o resultado, enquanto whenComplete apenas executa uma ação (por exemplo, log). Se você deseja alterar o resultado — use handle.
Erro nº 6: duplicar lógica de tratamento de erros. Muitas vezes é possível unificar o tratamento de erros em um único lugar para evitar duplicação de código — por exemplo, via um handle centralizado ou um manipulador comum.
GO TO FULL VERSION