1. El problema: excepciones en el código asíncrono
En el código habitual (sincrónico) todo es sencillo: si en un método ocurre una excepción, esta «salta» hacia arriba por la pila de llamadas y podemos capturarla con try-catch. Por ejemplo:
try {
int x = 1 / 0;
} catch (ArithmeticException ex) {
System.out.println("¡División por cero!");
}
En el código asíncrono la situación es más complicada. Cuando lanzamos una tarea mediante CompletableFuture.supplyAsync, se ejecuta en otro hilo. Si allí se produce una excepción, ¡no se lanzará en el hilo principal! En su lugar, se «empaquetará» dentro del objeto CompletableFuture y, si luego invocas get() o join(), recibirás esa excepción en forma de ExecutionException.
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// Uy, ¡aquí hay un error!
return 1 / 0;
});
try {
Integer result = future.get(); // aquí se lanzará una excepción
} catch (Exception ex) {
System.out.println("Se ha producido un error: " + ex.getMessage());
}
Pero si no llamas a get() (lo cual, por cierto, no es muy asíncrono) y en su lugar construyes cadenas con thenApply y otros métodos, el error puede «perderse». Por eso, en la programación asíncrona es muy importante saber capturar y manejar los errores directamente en las cadenas de CompletableFuture.
2. Método exceptionally: manejo de errores y valor de retorno
El método exceptionally te permite capturar una excepción si se produjo en etapas previas de la cadena, manejarla y devolver un valor alternativo. Es como catch, pero para el flujo de datos asíncrono.
Firma:
CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
Ejemplo de uso
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Ejecutamos un cálculo peligroso...");
if (Math.random() > 0.5) {
throw new RuntimeException("¡Algo ha salido mal!");
}
return 42;
});
future = future.exceptionally(ex -> {
System.out.println("Se ha producido un error: " + ex.getMessage());
return 0; // Devolvemos un valor «seguro»
});
Ejemplo con thenAccept
future.thenAccept(result -> System.out.println("Resultado: " + result));
Salida (aproximada):
Ejecutamos un cálculo peligroso...
Se ha producido un error: ¡Algo ha salido mal!
Resultado: 0
Ejecutamos un cálculo peligroso...
Resultado: 42
¡Importante! El método exceptionally se activa solo si en la cadena previa se produjo una excepción no manejada. Si todo va bien, simplemente «deja pasar» el resultado.
3. Método handle: manejador universal de resultado y error
A veces necesitamos manejar tanto el resultado como el error a la vez. Por ejemplo, si todo va bien — devolver el resultado; si hay error — devolver una alternativa o registrar el fallo en los logs.
Firma:
CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
- Primer argumento: el resultado (o null si hubo error),
- Segundo: la excepción (o null si todo fue bien).
Ejemplo de uso
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("¡Error aleatorio!");
return 100;
});
CompletableFuture<Integer> safeFuture = future.handle((result, ex) -> {
if (ex != null) {
System.out.println("Error detectado: " + ex.getMessage());
return -1;
}
return result;
});
safeFuture.thenAccept(r -> System.out.println("Resultado final: " + r));
Salida:
Error detectado: ¡Error aleatorio!
Resultado final: -1
Resultado final: 100
handle conviene cuando quieres actuar independientemente de cómo termine la tarea — con éxito o con error. Es un manejador universal de resultados que siempre se invoca y recibe dos argumentos: el resultado (si todo va bien) y la excepción (si algo salió mal).
El método es ideal si necesitas registrar errores de forma centralizada, devolver un valor por defecto sin romper la cadena o simplemente finalizar el escenario asíncrono de forma ordenada.
Ejemplo:
CompletableFuture<Integer> future = CompletableFuture
.supplyAsync(() -> 10 / 0) // aquí ocurrirá un error
.handle((result, ex) -> {
if (ex != null) {
System.out.println("Error: " + ex.getMessage());
return 0; // valor por defecto
}
return result;
});
System.out.println(future.join()); // imprimirá 0
A diferencia de exceptionally, que reacciona solo a los errores, handle se ejecuta siempre, lo que te permite tratar ambos desenlaces en un mismo lugar y mantener fluida toda la cadena.
4. Método whenComplete: acciones colaterales tras la finalización
A veces no necesitamos cambiar el resultado, sino simplemente ejecutar alguna acción después de que la tarea termine — por ejemplo, registrar que se ha completado, tanto si fue con éxito como si hubo error.
Firma:
CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
- Primer argumento: el resultado (o null si hay error),
- Segundo: la excepción (o null si hay éxito).
Ejemplo de uso
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("¡Error!");
return 10;
});
future.whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("Error durante la ejecución: " + ex.getMessage());
} else {
System.out.println("Finalizado con éxito, resultado: " + result);
}
});
Diferencia importante:
whenComplete no cambia el resultado ni el error; solo ejecuta una acción. Si en whenComplete se produce una excepción, esta se «adhiere» a la ya existente.
Ejemplo: registramos en logs, pero no intervenimos
future
.whenComplete((res, ex) -> {
System.out.println("Tarea finalizada. ¿Error? " + (ex != null));
})
.thenAccept(r -> System.out.println("Resultado para el usuario: " + r));
5. Particularidades y matices de implementación
Buenas prácticas: cómo manejar correctamente los errores en CompletableFuture
- Añade siempre manejo de errores (exceptionally, handle o whenComplete) a las cadenas de tareas asíncronas. De lo contrario, el error puede pasar desapercibido y la aplicación comportarse de forma impredecible.
- No uses get() o join() en el hilo principal sin try-catch: eso convertirá el código asíncrono en sincrónico y puede provocar bloqueos.
- Si necesitas devolver un «valor de reserva» ante un error, usa exceptionally o handle.
- Para efectos colaterales (logging, notificación al usuario), usa whenComplete.
- Puedes combinar en las cadenas: por ejemplo, primero manejar el error con exceptionally, luego registrar con whenComplete y después continuar el procesamiento del resultado.
- Recuerda que, si el error no se maneja, «fluirá» hasta la siguiente llamada a get()/join() y puede provocar la caída de la aplicación.
Orden de los métodos
- Si utilizas exceptionally, solo intercepta los errores que se produzcan antes de él en la cadena.
- Si tras exceptionally se vuelve a producir un error (por ejemplo, en thenApply), tendrás que manejarlo aparte.
- handle es universal: se ejecuta siempre, haya error o no.
Combinación de métodos
CompletableFuture.supplyAsync(() -> {
// ...
})
.handle((result, ex) -> {
if (ex != null) return "Error: " + ex.getMessage();
return result;
})
.whenComplete((res, ex) -> {
System.out.println("La tarea ha finalizado, resultado: " + res);
});
¿Qué pasa si no manejas el error?
Si la excepción no se maneja y llamas a get() o join(), se lanzará como ExecutionException (o CompletionException), y la aplicación podría finalizar con error.
6. Errores típicos al manejar errores en CompletableFuture
Error n.º 1: ausencia de manejo de errores. Si no añades ni exceptionally, ni handle, ni whenComplete, el error simplemente se «perderá» hasta la siguiente llamada a get()/join(), que puede estar muy lejos del lugar donde ocurrió.
Error n.º 2: usar get()/join() en el hilo principal sin try-catch. Esto convierte el código asíncrono en sincrónico y puede provocar bloqueos o caídas inesperadas de la aplicación.
Error n.º 3: comprensión incorrecta de dónde se activa el manejador. exceptionally captura solo los errores anteriores en la cadena. Si después de él vuelve a surgir un error, ese método no lo manejará.
Error n.º 4: manejar el error pero sin devolver un valor. En el método exceptionally o handle debes devolver un valor; de lo contrario, la siguiente etapa de la cadena recibirá null (o no recibirá nada).
Error n.º 5: confundir handle con whenComplete. handle puede cambiar el resultado, y whenComplete solo ejecutar una acción (por ejemplo, logging). Si quieres modificar el resultado, usa handle.
Error n.º 6: duplicar la lógica de manejo de errores. A menudo puedes unificar el manejo de errores en un solo lugar para evitar duplicar código, por ejemplo mediante un handle centralizado o un manejador común.
GO TO FULL VERSION