La asincronia un viaje desde futures a promesas

10-01-2025

Por Aitor Santana Cabrera

En el desarrollo de software moderno, la asincronía es un pilar esencial, sobre todo en aplicaciones web en las que dependemos de la respuesta de APIs o servicios externos. Aquellos que estamos acostumbrados a programar web en JavaScript/TypeScript, conocemos las Promesas, que nos permiten manejar esas situaciones asíncronas que mencionaba antes. Sin embargo, estos últimos meses que he estado trabajando más con Java y Spring me surgió la duda, ¿habrá algo similar para este lenguaje? La respuesta es que sí y se llama Future.

Futures en Java: La Base de la Asincronía

Java, un lenguaje conocido por su robustez y capacidad de manejo de múltiples hilos, introduce la interfaz Future en Java 5 para representar resultados de operaciones asíncronas. Sin embargo, Future presenta limitaciones, especialmente por su naturaleza bloqueante y la falta de flexibilidad para manejar cadenas de operaciones o errores de forma eficiente. Es por eso que en Java 8 fue introducido CompletableFuture, una clase que implementa Future y CompletionStage y aborda muchas de las limitaciones de Future.

CompletableFuture

CompletableFuture proporciona una API rica para componer, combinar y manejar operaciones asíncronas, ofreciendo métodos como thenApply, thenCombine, y exceptionally para poder manejar de una forma más flexible los diferentes estados asíncronos. Entremos en más detalle sobre los métodos principales que tiene esta estructura:

  • thenApply: Se utiliza para transformar el resultado de un CompletableFuture una vez que se completa. Funciona de manera similar a la función map en streams, aplicando una función al resultado del Future.
  • thenAccept: Se usa para consumir el resultado de la operación asíncrona una vez que se completa. A diferencia de thenApply, que aplica una función y devuelve un CompletableFuture, thenAccept toma un Consumer y no devuelve nada (su tipo de retorno es void). Es útil cuando simplemente quieres realizar una acción con el resultado de la operación asíncrona, como imprimirlo o procesarlo de alguna manera, sin transformarlo o devolver otro valor.
  • exceptionally: Se utiliza para manejar excepciones que pueden ocurrir durante la ejecución de un CompletableFuture. Funciona como un bloque catch, permitiendo definir un comportamiento en caso de que surja una excepción en la operación asíncrona.

Veámos un ejemplo de como podríamos hacer una petición http con esta estructura:

import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class AsyncHttpRequest { public static void main(String[] args) throws ExecutionException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://rickandmortyapi.com/api/character")) .build(); CompletableFuture<HttpResponse<String>> responseFuture = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()); responseFuture .thenApply(HttpResponse::body) .thenAccept(System.out::println) .exceptionally(e -> { System.out.println("Hubo un error en la petición: " + e.getMessage()); return null; }) .join(); // Espera a que se complete la operación } }

Existe otro método muy interesante, thenCombine que permite combinar dos CompletableFutures. Realiza dos operaciones de manera independiente y luego, combina sus resultados una vez que ambas se han completado, en una función que le pasemos como segundo parámetro. Veamoslo con un ejemplo:

import java.util.concurrent.CompletableFuture; public class ThenCombineExample { public static void main(String[] args) { CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> { // Simula una tarea que calcula el cuadrado return 2 * 2; // 4 }); CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> { // Simula una tarea que calcula el cubo return 3 * 3 * 3; // 27 }); CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (square, cube) -> { // Combina los resultados (4 + 27) return square + cube; }); combinedFuture.thenAccept(total -> System.out.println("Total: " + total)); // Muestra "Total: 31" } }

Promesas en JavaScript

En JavaScript la asincronía se maneja a través de Promesas. Introducidas como una mejora sobre los callbacks anidados en ES6 del año 2015. Las Promesas proporcionan una forma más limpia y manejable de organizar operaciones asíncronas. Con métodos como then, catch, y finally, así como las palabras reservadas async/await, que nos permiten simplificar el tratamiento de operaciones asíncronas y la gestión de errores.

  • then: Se utiliza para especificar qué hacer cuando la Promesa se resuelve correctamente. Recibe una función que se ejecuta con el valor resuelto de la Promesa. Es posible encadenar varios then para transformar los valores o encadenar operaciones asíncronas.
  • catch: Se emplea para manejar errores o rechazos en la Promesa. Recibe una función que se ejecuta si la Promesa es rechazada, o si ocurre un error en alguna parte de la cadena de Promesas.
  • finally: Se usa para ejecutar código independientemente de si la Promesa se resuelve o se rechaza. No recibe el resultado o el error de la Promesa, pero es útil para ejecutar lógica de limpieza o finalización, como cerrar conexiones o limpiar recursos.

Veamos el ejemplo de petición anterior en JavaScript:

// Sólo con Promesas fetch("https://rickandmortyapi.com/api/character") .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error("Error en la petición:", error)); // Con async/await try { const response = await fetch("https://rickandmortyapi.com/api/character"); const characters = await response.json(); } catch (error) { console.error("Error en la petición:", error); }

CompletableFuture VS Promesas

Ambas estructuras tienen un estilo bastante similar, su objetivo es el mismo, sin embargo difieren en 3 puntos importantes:

  1. Funcionalidades y Métodos: CompletableFuture ofrece una gama más amplia de métodos para el manejo de asincronía, incluyendo combinación y composición de múltiples Futures, además de la ejecución asíncrona. Las Promesas en JavaScript se centran en simplificar el flujo de operaciones asíncronas y el manejo de errores.
  2. Manejo de Errores: En Java, el manejo de errores con CompletableFuture puede ser más detallado, aprovechando las capacidades de manejo de excepciones de Java. En JavaScript, el manejo de errores con Promesas es más sencillo, utilizando principalmente .catch().
  3. Concurrencia: Java, con su modelo de concurrencia basado en hilos, permite un control más detallado sobre el paralelismo y la asincronía. En JavaScript, esto se maneja a través del Event Loop, que está enfocado en ser no bloqueante y en el uso eficiente del único hilo.

Conclusión

En resumen, el viaje desde CompletableFuture en Java hasta las Promesas en JavaScript, nos revela cómo lenguajes diferentes abordan la asincronía de formas únicas, cada uno con sus fortalezas. Mientras Java ofrece un enfoque robusto y detallado, ideal para aplicaciones con fuerte tipado y concurrencia, JavaScript brinda una solución elegante y adaptable, perfecta para el dinamismo de la web. Esta exploración, no solo enriquece nuestra caja de herramientas como desarrolladores, sino que también nos invita a reflexionar: ¿Cómo pueden estos paradigmas influir en nuestra forma de pensar y resolver problemas en la programación? Al final, comprender estas diferencias y similitudes no sólo nos hace mejores en un lenguaje, sino más adaptativos y hábiles en el arte de la programación.