8 Gestión de excepciones¶
Introducción¶
Internamente, un CompletableFuture
contiene dos canales distintos, el canal de datos en el que se almacenan los resultados de la cadena de operaciones, y el canal de error, en el que albergan las excepciones generadas en la cadena de operaciones.
Si alguna tarea de la cadena de tareas genera una excepción, el método isCompletedExceptionally()
del CompletableFuture
retornará true
, el método get()
producirá una excepción ExecutionException
, y los métodos join()
y getNow()
producirán una excepción CompletionException
.
Por defecto, las operaciones posteriores a la que generó el error dentro de la cadena de operaciones NO serán ejecutadas, ya que el canal de datos de la cadena deja de producir datos tras la excepción, que se propaga por el canal de error de la cadena de operaciones.
Veamos un ejemplo:
private void exceptionExample() {
CompletableFuture<Void> cf =
CompletableFuture.supplyAsync(this::generateNumber)
.thenApplyAsync(this::duplicate)
.thenAcceptAsync(this::printNumber);
System.out.printf("%s - Main\n", Thread.currentThread().getName());
// Esta operación lanza la excepción CompletionException porque
// el CompletableFuture resultante de la cadena de operaciones
// fue completado con una excepción.
cf.join();
}
private int generateNumber() {
System.out.printf("%s - Supplier\n", Thread.currentThread().getName());
throw new RuntimeException();
}
private Integer duplicate(Integer value) {
int duplicated = value * 2;
System.out.printf("%s - Function - Duplicated: %d\n",
Thread.currentThread().getName(), duplicated);
return duplicated;
}
private void printNumber(Integer value) {
System.out.printf("%s - Consumer - %d\n",
Thread.currentThread().getName(), value);
}
Manejo de valor o excepción (handle)¶
Si queremos gestionar una excepción lanzada en la cadena de operaciones podemos usar el método handleAsync(biFunction)
, a la que pasamos una biFunction que recibe el valor con el que se ha completado el CompletableFuture y el Throwable si ha sido completado con excepción, uno de los cuales será null
.
La biFunction será ejecutada en un hilo del ForkJoinPool.commonPool()
, y deberá retornar el nuevo resultado con el que se debe seguir la cadena de operaciones, permitiendo así la recuperación del error. Si no queremos recuperarnos de error o lanzar otro distinto, simplemente deberemos de hacer que la biFunction lance una excepción.
Este método se encuentra sobrecargado, handleAsync(biFunction, executor)
, para recibir el executor en el que queremos que se ejecute la biFunction.
Método handle con ejecutor
También existe el método handle(biFunction, executor)
, pero debemos tener en cuenta que bajo ciertas circunstancias es posible que la biFunction se ejecute en el hilo principal, como vimos anteriormente en el caso de thenRun(runnable)
.
El método handleAsync(biFunction)
es llamado siempre, tanto si la operación anterior ha sido completada correctamente como si se ha generado una excepción, lo que determinará cuál de los dos parámetros de la biFunction recibirá un valor real y cuál recibirá null
.
private void handleExample() {
CompletableFuture<Void> cf =
CompletableFuture.supplyAsync(this::generateNumber)
.handle(this::handler)
.thenApplyAsync(this::duplicate)
.thenAcceptAsync(this::printNumber);
System.out.printf("%s - Main\n", Thread.currentThread().getName());
cf.join();
}
private int generateNumber() {
System.out.printf("%s - Supplier\n", Thread.currentThread().getName());
int value = ThreadLocalRandom.current().nextInt(10) + 1;
if (value > 5) {
throw new RuntimeException();
} else {
return value;
}
}
private Integer handler(Integer value, Throwable throwable) {
Integer valueToReturn = value;
if (throwable != null) {
valueToReturn = 100;
}
System.out.printf("%s - Handler returning %d\n",
Thread.currentThread().getName(), valueToReturn);
return valueToReturn;
}
private Integer duplicate(Integer value) {
int duplicated = value * 2;
System.out.printf("%s - Function - Duplicated: %d\n",
Thread.currentThread().getName(), duplicated);
return duplicated;
}
private void printNumber(Integer value) {
System.out.printf("%s - Consumer - %d\n",
Thread.currentThread().getName(), value);
}
Manejo exclusivo de la excepción (exceptionally)¶
Si queremos que se ejecute una determinada function sólo cuando se produzca una excepción en la cadena de operaciones, de manera que podamos recuperarnos del error, podemos hacer uso del método exceptionally(function)
, a la que pasaremos una function que recibe el throwable correspondiente a la excepción lanzada, y que creará una nuevo CompletableFuture que será completado con el valor retornado por la function, de manera que la cadena de operaciones puede proseguir a partir de ese punto, recuperándonos de error.
Si en vez de de recuperarnos de error lo que queremos es lanzar uno distintos, simplemente deberemos hacer que la function lance una nueva excepción.
Debemos tener en cuenta que el método exceptionally()
sólo será llamado si hay alguna excepción en el canal de error del CompletableFuture
al que se aplica.
private void exceptionallyExample() {
CompletableFuture<Void> cf =
CompletableFuture.supplyAsync(this::generateNumber)
.exceptionally(this::recover)
.thenApplyAsync(this::duplicate)
.thenAcceptAsync(this::printNumber);
System.out.printf("%s - Main\n", Thread.currentThread().getName());
cf.join();
}
private int generateNumber() {
System.out.printf("%s - Supplier\n", Thread.currentThread().getName());
int value = ThreadLocalRandom.current().nextInt(10) + 1;
if (value > 5) {
throw new RuntimeException();
} else {
return value;
}
}
private Integer recover(Throwable throwable) {
int recoveringValue = 100;
System.out.printf("%s - Recovering to %d\n",
Thread.currentThread().getName(), recoveringValue);
return recoveringValue;
}
private Integer duplicate(Integer value) {
int duplicated = value * 2;
System.out.printf("%s - Function - Duplicated: %d\n",
Thread.currentThread().getName(), duplicated);
return duplicated;
}
private void printNumber(Integer value) {
System.out.printf("%s - Consumer - %d\n",
Thread.currentThread().getName(), value);
}
En el ejemplo anterior, nos recuperamos del error producido en el método generateNumber()
haciendo que la function pasada al método exceptionally(function)
retorne el valor 100
, produciendo un CompletableFuture completado con dicho valor, de manera que la cadena de operaciones puede continuar
Las operaciones de la cadena posteriores a la excepción y anteriores al método exceptionally(function)
no serán ejecutadas, por lo que si queremos tener control sobre cada operación podemos incluir una operación exceptionally(function)
después de cada operación que pueda lanzar una excepción.
A partir de Java 12 tenemos disponible el método exceptionallyAsync(function)
, similar a exceptionally(function)
pero en la que la function es ejecutada en un un hilo del ForkJoinPool.commonPool()
, evitando así que bajo ciertas circunstancias pueda ser ejecutada en el hilo principal, como ocurre con exceptionally(function)
.
Este método se encuentra sobrecargado, exceptionallyAsync(function, executor)
, para recibir el executor en el que queremos que se ejecute la function.
Manejo exclusivo de la excepción ejecutando otro CompletableFuture¶
Si el valor de recuperación ante una excepción proviene de la ejecución de otra cadena de operaciones, usar exceptionally(function)
puede no ser la mejor opción, ya que si la function retorna un CompletableFuture en vez de un simple valor el CompletableFuture resultante sería del tipo CompletableFuture<CompletableFuture<T>>
, dificultándonos el encadenamiento de más operaciones.
Por este motivo, Java 12 introdujo el método exceptionallyComposeAsync(function)
, que permite a la function retornar un CompletableFuture, cuyo valor será usado internamente para crear el Completable retornado por el método, cuyo tipo será CompletableFuture<T>
. La function será ejecutada en un hilo del ForkJoinPool.commonPool()
.
Este método se encuentra sobrecargado, exceptionallyComposeAsync(function, executor)
, para recibir el executor en el que queremos que se ejecute la function.
exceptionallyCompose
También existe el método exceptionallyCompose(function)
, pero debemos tener en cuenta que bajo ciertas circunstancias es posible que la function se ejecute en el hilo principal, como vimos anteriormente en el caso de thenRun(runnable)
.
Debemos tener en cuenta que el método exceptionallyComposeAsync()
sólo será llamado si hay alguna excepción en el canal de error del CompletableFuture
al que se aplica.
private void exceptionallyComposeAsyncExample() {
CompletableFuture<Void> cf =
CompletableFuture.supplyAsync(this::generateNumber)
.exceptionallyComposeAsync(this::recover)
.thenApplyAsync(this::duplicate)
.thenAcceptAsync(this::printNumber);
System.out.printf("%s - Main\n", Thread.currentThread().getName());
cf.join();
}
private int generateNumber() {
System.out.printf("%s - Supplier\n", Thread.currentThread().getName());
int value = ThreadLocalRandom.current().nextInt(10) + 1;
if (value > 5) {
throw new RuntimeException();
} else {
return value;
}
}
private CompletableFuture<Integer> recover(Throwable throwable) {
System.out.printf("%s - Recovering\n",
Thread.currentThread().getName());
return CompletableFuture.supplyAsync(this::generateRecoveringValue);
}
private int generateRecoveringValue() {
int recoveringValue = 100;
System.out.printf("%s - Generating recovery value %d\n",
Thread.currentThread().getName(), recoveringValue);
return recoveringValue;
}
private Integer duplicate(Integer value) {
int duplicated = value * 2;
System.out.printf("%s - Function - Duplicated: %d\n",
Thread.currentThread().getName(), duplicated);
return duplicated;
}
private void printNumber(Integer value) {
System.out.printf("%s - Consumer - %d\n",
Thread.currentThread().getName(), value);
}
Manejo de la excepción sin recuperación¶
En algunas ocasiones simplemente queremos saber si se ha obtenido un resultado o se ha propagado un error producido en alguna de las operaciones anteriores. Para gestionar este caso tenemos disponible el método whenCompleteAsync(biFunction)
, que ya conocemos cuya biFunction recibe un resultado y un Throwable, uno de los cuales será null
.
La diferencia principal con respecto a handle(biFunction)
es que no trata de recuperarse de la excepción (no es necesario retornar nada) y si no se ha producido un error y se ha obtenido resultado, automáticamente el CompletableFuture
resultante será completado con dicho resultado, sin que nosotros tengamos que retornarlo como hacíamos con handle(biFunction)
.