CodeGym /Cursos /JAVA 25 SELF /Tratamento de erros em código assíncrono: exceptionally, ...

Tratamento de erros em código assíncrono: exceptionally, handle

JAVA 25 SELF
Nível 55 , Lição 3
Disponível

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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION