Promesas y Async/Await en JavaScript

Promesas y Async/Await en JavaScript

El desarrollo de aplicaciones JavaScript modernas requiere manejar operaciones asíncronas de forma eficiente, cuando el código necesita acceder a recursos como APIs, bases de datos o archivos, se debe evitar bloquear la ejecución mientras se esperan las respuestas y técnicas como callbacks, promesas y async/await permiten que nuestro código siga fluyendo mientras se realizan tareas en segundo plano, dominar y comprender estas técnicas es clave para crear aplicaciones JavaScript escalables y de alto rendimiento.

Algo de Historia: XMLHttpRequest

En un principio o al menos cuando yo empecé con el desarrollo web, para realizar una solicitud HTTP asíncrona, creábamos una instancia de XMLHttpRequest, configurábamos la URL, el método HTTP y los headers, luego adjuntábamos un callback usando onreadystatechange para manejar la respuesta cuando estuviera lista.

// Crear instancia
const xhr = new XMLHttpRequest(); 

// Configurar solicitud
xhr.open('GET', 'example.com/data');

// Configurar headers (opcional)
xhr.setRequestHeader('Content-Type', 'application/json');

// Manejar respuesta asíncrona 
xhr.onreadystatechange = function() {
  if(xhr.readyState === 4 && xhr.status === 200) {
    // Lógica para manejar respuesta exitosa
    const responseData = JSON.parse(xhr.responseText);
  } else if(xhr.readyState === 4) {
    // Manejar error
  }
}

// Enviar solicitud
xhr.send();

Pero había varios problemas con este enfoque:

  • Mucho código boilerplate para configurar cada solicitud.
  • Callbacks anidados podían causar Callback Hell.
  • Manejo manual de errores en cada callback.
  • Inconsistencias entre navegadores antiguos (esto solía ser uno de los problemas más complejos a resolver).

XMLHttpRequest fue un avance en su momento, pero hacía falta una API más limpia y consistente para tratar con asincronía. Esta necesidad motivó el desarrollo de las Promesas.

Para abordar estos problemas, surgieron populares librerías AJAX populares como jQuery y otras que envolvían a XHR en una API más simple y consistente. Pero estas soluciones seguían basándose en callbacks para el manejo asíncrono.

Con el tiempo, XHR y las librerías AJAX fueron reemplazados por APIs nativas más modernas como Fetch API y Axios. Pero incluso usando estas APIs, el manejo de asincronía mediante callbacks anidados seguía siendo un desafío.

Fue en este contexto que llegaron las Promesas en ES6, como una forma estándar integrada en el lenguaje para abstraer la asincronía de una manera más manejable.

Promesas (Promises)

Las promesas representan el resultado eventual de una operación asíncrona. Fueron introducidas en la especificación de ECMAScript 6 (ES6) y rápidamente adoptadas por los navegadores modernos y runtimes como Node.js.

Las promesas permiten escribir código asíncrono de una manera más limpia que con callbacks anidados. Esto es posible gracias a que tienen una API bien definida para encadenar y combinar operaciones asíncronas.

Algunas características importantes de las promesas:

  • Una promesa puede tener tres estados: pendiente, cumplida o rechazada.

  • Devuelven un objeto promesa donde se adjuntan callbacks, no se pasan como argumentos.

  • Los métodos .then(), .catch() y .finally() permiten encadenar lógica adicional.

  • Se pueden componer múltiples promesas usando Promise.all() y Promise.race().

  • Una vez resuelta o rechazada, la promesa se "cristaliza" y su estado no cambia.

  • Las promesas se resolven de forma asíncrona, no bloqueando el hilo principal.

Estados de una Promesa

  • Pending: Estado inicial.
  • Fulfilled: Operación completada con éxito.
  • Rejected: Operación fallida.

Un ejemplo de una promesa básica

const unaFuncion = (resolve, reject) => {
  if(todoOK) {
    resolve('¡Éxito!'); 
  } else {  
    reject('Error!');
  }
};

const miPromesa = new Promise(unaFuncion);

Async/Await

Async/await es una forma más reciente de trabajar con asincronía en JavaScript que se introdujo en ECMAScript 2017 (ES7).

Async/await se basa en promesas y les permite una forma más directa de escribir código asíncrono secuencial, evitando encadenamientos de .then().

La palabra clave async define una función asíncrona, que devuelve implícitamente una Promise como resultado incluso si no se retorna explícitamente una.

Dentro de una función async, podemos usar await para pausar la ejecución hasta que una Promise se resuelva. Aquí es donde entra en juego fetch, una función incorporada en los navegadores que se utiliza para realizar solicitudes HTTP asíncronas y obtener recursos de servidores web.

La función fetch toma como argumento una URL (Uniform Resource Locator) que apunta al recurso que deseamos recuperar, por ejemplo:

async function obtenerUsuarioAsync() {
  // Realiza una solicitud HTTP GET a la URL especificada
  const response = await fetch('/api/user');

  // La variable "response" contiene la respuesta de la solicitud
  // que incluye información como el estado de la respuesta, encabezados y el cuerpo de la respuesta.

  // A continuación, usamos "await" nuevamente para obtener el contenido JSON de la respuesta.
  const user = await response.json();

  // Finalmente, devolvemos los datos del usuario obtenidos de la API.
  return user;
}

Usando async/await se puede escribir código asíncrono como si fuera síncrono, en donde las llamadas a funciones async con await pausan la ejecución hasta que devuelven un resultado.

Esto hace que el código asíncrono sea más fácil de leer y escribir que encadenando .then(). Pero async/await sigue basándose en promesas detrás de escena.

Async/await es simplemente "sintactic sugar" para trabajar con promesas de una forma más limpia y directa.

Bonus: Evitando el bloqueo del hilo en Node.js

Pero el uso de async/await también introduciría un problema y es que provoca que el hilo principal en Node.js de bloquee, por ello debemos asegurarnos de que las operaciones intensivas en recursos o que tardan mucho tiempo se manejen de manera asíncrona.

// ESTO BLOQUEARÁ EL HILO PRINCIPAL
async function bloqueante() {
  // CPU-intensive task
  const datos = await heavyliftingsync(); 
  return datos;
}

El problema es que heavyliftingsync() se ejecuta de forma síncrona, bloqueando la ejecución. En su lugar, debemos:

// FORMA CORRECTA
async function noBloqueante() {
  const datos = await heavyliftingAsync();
  return datos; 
}

function heavyliftingAsync() {
  return new Promise((resolve, reject) => {
    heavyLiftingAsync(resolve); 
  });
}

Aquí heavyliftingAsync() se ejecuta de forma asíncrona, permitiendo que Node continúe la ejecución mientras se completa en segundo plano.

Async/await no bloquea por sí mismo, el problema viene de ejecutar tareas intensivas de forma síncrona. Con async/await podemos orquestar fácilmente tareas asíncronas sin riesgo de bloquear.

Uso conjunto de Promesas y Async/Await

El siguiente ejemplo muestra cómo se pueden combinar Promesas y Async/Await para hacer una serie de llamadas a una API.

// Promesa para obtener datos de API
function obtenerDatos(url) {
  return new Promise((resolve, reject) => {
    // llamada asincrónica a API
    setTimeout(() => resolve(`Datos de ${url}`), 1000);
  }); 
}

async function obtenerDatosDeAPIs() {
  try {
    const promesas = [
      obtenerDatos('/api1'),
      obtenerDatos('/api2'),
      obtenerDatos('/api3')
    ];

    // Promise.all ejecuta las promesas concurrently
    const [datos1, datos2, datos3] = await Promise.all(promesas);
    prepararDatos(datos1, datos2, datos3);
  } catch (error) {
     // manejar error   
  }
}

obtenerDatosDeAPIs();

En este ejemplo:

  • La función obtenerDatos() devuelve una promesa que resuelve con los datos de cada API.

  • Creamos un array de promesas a ejecutar concurrentemente.

  • Usamos Promise.all() para ejecutar todas las promesas en paralelo y esperar por los resultados.

  • Async/await nos permite trabajar con el resultado de Promise.all() de forma directa con la sintaxis await.

  • Manejamos los resultados con desestructuración en las constantes datos1, datos2, datos3.

  • Si ocurre un error, lo capturamos con try/catch.

De esta manera combinamos la ejecución concurrente de Promise.all() con la sintaxis limpia de async/await para obtener y trabajar con los datos de forma eficiente y legible.

Conclusión

Bueno, es importante aclarar que async/await en sí mismo no bloquea el hilo principal de ejecución en JavaScript, ya que JavaScript es un lenguaje de programación de un solo hilo con entrada y salida no bloqueantes.

Cuando usas await, lo que haces es pausar solamente la ejecución de la función async en la que se encuentra, hasta que la promesa correspondiente se resuelva o se rechace. Esto no significa que el hilo principal esté bloqueado. Otras operaciones y eventos todavía pueden procesarse en segundo plano mientras la función async está en pausa.

La preocupación sobre el bloqueo del hilo principal es más pertinente en entornos como Node.js, donde un mal diseño (como operaciones de CPU intensivas) podría efectivamente bloquear el hilo principal. Pero esto no es una característica de async/await per se, sino más bien una cuestión de cómo se utiliza.

Happy coding! :D


Photo by Pavan Trikutam on Unsplash

Jack Fiallos

Jack Fiallos

Te gustó este artículo?