Promesas en JavaScript: Qué son y cómo se usan

Javascript

Las promesas, utilizadas para gestionar el código asíncrono, se han convertido en un componente fundamental de JavaScript. En este tutorial aprenderás qué son las promesas de JavaScript y cómo se utilizan. El uso de  promesas es muy habitual cuando usas alguna API web que devuelve una promesa, como por ejemplo la API Fetch de JavaScript.

Qué es una promesa

Las promesas de JavaScript son uno de los métodos utilizados para gestionar el código asíncrono sin la necesidad de abusar de callbacks. El abuso de funciones callback anidadas trae como consecuencia el callback hell, motivo por el cual el código se vuelve complicado e ilegible.

Las promesas han estado presentes durante años, aunque no comenzaron a formar parte del estándar de JavaScript hasta su versión ES2015.

Una promesa representa la finalización de una función asíncrona. Es un objeto que podría devolver un valor en el futuro, cumpliendo el mismo rol que una función que podríamos usar como callback de otra función.  Sin embargo, las promesas incluyen más funcionalidades y disponen de una sintaxis mucho más legible.

Lo más habitual es que como desarrollador, uses las promesas que incluyen las librerías que vayas a utilizar, ya que las APIs asíncronas de los navegadores suelen devolver promesas como resultado. Tendrás que crear el código que consuma dichas promesas, aunque en ocasiones también las tendrás que crear.

Cuando una función llama a una promesa, esta pasará a tener un estado pendiente que indicará que todavía no se ha completado. La función que ha iniciado la promesa continuará su ejecución normalmente mientras espera a que la promesa finalice su ejecución.

La función que ha llamado a la promesa espera a que la promesa se revuelva con un resultado, pero en todo caso continuará su ejecución. Es algo así como si tu jefe te encarga una tarea y continúa trabajando en sus cosas mientras a su vez espera a que completes la tarea. Las promesas son usadas por APIs como la API Fetch de JavaScript o los service workers. Lo más habitual es que hoy en día trabajes con promesas cuando usas JavaScript.

Cómo crear una promesa

Puedes crear una promesa usando directamente su constructor, mediante la sentencia new Promise, que acepta como parámetro una función con dos parámetros que gestionan el éxito o el fracaso en la operación:

const promesa = new Promise((resolve, reject) => {
  // ...
});

Si inspeccionas el código de la promesa en tu navegador verás que está en estado pending y que su valor es undefined:

__proto__: Promise
[[PromiseStatus]]: "pending"
[[PromiseValue]]: undefined

La promesa anterior no nos sirve de nada si no establecemos un resultado para la misma que cambie su estado. Vamos a modificar la promesa anterior para resolverla:

const promesa = new Promise((resolve, reject) => {
  resolve('Resuelta!')
});

Si ahora inspeccionas el código de nuevo verás que su estado es el de fulfilled y que su valor es el que hemos devuelto en la sentencia resolve:

__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: "Resuelta!"

Una promesa puede tener tres posibles estados:

  • ⏳ pending: Estado inicial, antes de que la promesa haya sido resulta o rechazada.
  • ❌ rejected: Ha habido un error en la operación y se ha rechazado.
  • ✅ fulfilled: La operación se ha completado con éxito.

A continuación vamos a crear otra promesa que resolveremos o rechazaremos en función de ciertos parámetros:

let completado = true;

const tareaCompletada = new Promise((resolve, reject) => {
  if (completado) {
    const trabajoCompletado = 'He completado la tarea';
    resolve(trabajoCompletado);
  } else {
    const trabajoFallido = 'No he podido completar la tarea';
    reject(trabajoFallido);
  }
});

En el ejemplo anterior comprobamos el estado de la variable local completado. Si su valor es true, resolvemos la tarea o, de lo contrario, rechazamos la promesa. Devolvemos una simple cadena de texto, pero podríamos devolver una estructura más compleja, como por ejemplo un objeto.

Cómo consumir una promesa

En los ejemplos anteriores hemos completado las promesas con un valor, pero querremos comprobarlo para actuar en consecuencia. Por ello, las promesas disponen del método then, que se ejecutará cuando se resuelva la promesa.

A continuación usamos el método then, que recibe una función anónima como argumento que muestra el resultado de la promesa por la consola:

const promesa = new Promise((resolve, reject) => {
  resolve('Resuelta!')
});

promesa.then(response => {
  console.log(response);
});

El valor devuelto por la promesa del ejemplo anterior es Resuelta!. La promesa pasará este valor a la función anónima que definamos como respuesta, mostrando el resultado por la consola:

Resuelta!

En nuestros ejemplos anteriores no hemos usado código asíncrono. Vamos a crear otra promesa que use la función asíncrona setTimeOut() y luego vamos a mostrar su resultado:

const promesa = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Resolviendo petición asíncrona'), 5000);
});

promesa.then(response => {
  console.log(response);
});

Este será el resultado que se mostrará por la consola al cabo de 5000 milisegundos:

Resolviendo petición asíncrona

Al usar el método then nos aseguramos de que la respuesta de la promesa solamente será mostrada por pantalla cuando el temporizador de función setTimeout() finalice, al cabo de 5 segundos. Visualmente, el código resulta ahora más legible que cuando encadenamos callbacks.

Gestión de errores en promesas

Cuando estas promesas son resueltas tienen el estado fulfilled. Sin embargo, también debemos tener en cuenta el caso de que una promesa falle. Esto es habitual cuando una API no está disponible, falla o no tenemos autorización a la misma.

Una promesa debería ser capaz de gestionar también los errores. Cuando se produzca un error, el estado de la promesa pasará a ser rejected. En el siguiente ejemplo vamos a simular un posible resultado de una petición API a un servidor usando la función setTimeout():

const correcto = true;

const obtenerUsuarios = new Promise((resolve, reject) => {
    
  setTimeout(() => {
    if (correcto) {
      resolve([
        { id: 1, nombre: 'Eduardo', apellido: 'Lázaro' },
        { id: 2, nombre: 'Manuel', apellido: 'Rodríguez' },
      ]); 
    } else {
      reject('Error obteniendo los datos!');
    }
  }, 1000);
});

En el ejemplo anterior devolvemos una serie de objetos si todo ha salido correctamente. De haber algún error, devolvemos un mensaje de error. Para ello usamos el método catch, disponible en las promesas de JavaScript, que acepta el error devuelto como parámetro:

obtenerUsuarios.then(response => {
    console.log(response);
  }).catch(error => {
    console.error(error)
});

Este sería el resultado del código anterior:

(2) [{…}, {…}]
0: { id: 1, nombre: "Eduardo", apellido: "Lázaro" }
1: { id: 2, nombre: "Manuel", apellido: "Rodríguez" }

Si el valor de la variable global correcto fuese false, este sería el resultado:

Error obteniendo los datos!

Si ocurre cualquier error en la promesa, se ejecutará el código del método catch. Además del uso de reject, también podemos usar sentencia throw para generar un error manualmente:

new Promise((resolve, reject) => {
  throw new Error('Ha ocurrido un error');
}).catch(error => {
  console.error(error);
});

new Promise((resolve, reject) => {
  reject('Ha ocurrido un error');
}).catch(error => {
  console.error(error);
});

También es posible usar varios catch en cascada, aunque si se genera un error, siempre se ejecutará primero el más cercano:

new Promise((resolve, reject) => {
  throw new Error('Error');
}).catch(error => {
  throw new Error(error + ' grave');
}).catch(error => {
  console.error(error);
});

La promesa anterior mostrará el siguiente mensaje por la consola:

Error grave

Cómo encadenar promesas

Las promesas también pueden encadenarse, pasando el resultado recibido por una promesa a otra promesa, realizando así varias operaciones asíncronas. En caso de que el método then devuelva un valor, puedes agregar otro then de forma que acepte como argumento el valor devuelto por el anterior.

En el siguiente ejemplo encadenamos un segundo then:

const promesa = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Resolviendo petición asíncrona'), 5000);
});

promesa.
  then(primeraRespuesta => {
    return primeraRespuesta + ' encadenada con otra respuesta';
  })
  .then(segundaRespuesta => {
    console.log(segundaRespuesta);
  });

El segundo then mostrará por la consola el siguiente mensaje:

Resolviendo petición asíncrona encadenada con otra respuesta

Como then devuelve una promesa, lo que haces al usar varios es encadenar varias promesas. Dado que podemos usar los then sin que tengan que estar anidados, el código resulta más legible que cuando usamos callbacks.

Un ejemplo de ello es la API Fetch, que que permite realizar peticiones asíncronas a otros servidores. A su vez hace uso de la API XMLHttpRequest, que podemos usar obtener recursos y crear una cadena de promesas que se ejecutarán cuando se obtenga el recurso.

const verificarEstado = response => {
  // Comprobamos si hemos recibido un código de error
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response);
  }
  
  // Devolvemos un error
  const error = new Error(response.statusText);
  return Promise.reject(error);
}

const transformarRespuesta = response => response.json();

// Iniciamos la petición
fetch('https://api.github.com/users/edulazaro')
  .then(verificarEstado)
  .then(transformarRespuesta)
  .then(data => {
    console.log('Se ha obtenido una respuesta', data)
  })
  .catch(error => {
    console.log('Error en la petición', error);
  });

En el ejemplo hemos usado la función fetch para obtener los datos de un usuario desde la API REST de GitHub. Cuando se obtengan los datos se generará una respuesta que tiene diversas propiedades, entre las cuales encontramos las siguientes:

  • status: Contiene el código de estado HTTP de la respuesta.
  • statusText: Contiene el mensaje de estado de la respuesta.

El objeto response devuelto por la API contiene el método json(), que devuelve una promesa que será resuelta con el body de la respuesta HTTP transformado a formato JSON.

La primera promesa de la cadena ejecuta la función verificarEstado(), que comprueba el estado de la respuesta mediante el método status(), que devolverá un código HTTP. Si el código está entre los valores 200 y 299, entonces la respuesta ha sido exitosa. De lo contrario rechazamos la promesa y lanzamos un error que se saltará el resto de promesas, ejecutándose el catch más cercano.

Si todo ha salido bien, entonces se ejecutará la función transformarRespuesta() que hemos definido. Dado que la promesa anterior se ha resuelto con éxito, usamos el resultado como parámetro de la segunda. Dado que transformamos la respuesta a formato JSON, la tercera respuesta recibe código JSON que podemos mostrar por la consola.

Métodos disponibles en las promesas

Ya hemos visto ejemplos de uso de los métodos then y catch, pero también tenemos disponible el método finally, cuyo código se ejecutará una vez se resuelva la promesa, independientemente de si se ha completado con éxito o se ha rechazado.

El método finally de la promesa del siguiente ejemplo siempre se ejecutará:

obtenerUsuarios.then(response => {
    console.log(response);
  }).catch(error => {
    console.error(error);
  }).finally( () => {
  console.error('Esto siempre se ejecutará'); 
}); 

A continuación puedes ver una lista con los handlers de las promesas o métodos que te permtirán gestionar el resultado de las mismas:

Método Descripción
then Se usa para gestionar el estado fulfilled de una promesa y ejecuta la función onFulfilled de forma asíncrona.
catch Se usa para gestionar el estado rejected de una promesa y ejecuta la función onRejected de forma asíncrona.
finally Se usa para gestionar cualquier resultado de una promesa y ejecuta la función onFinally de forma asíncrona.

Cómo sincronizar de promesas

Puedes agrupar varias promesas usando el método all, que acepta un array de promesas como parámetros. Las promesas agrupadas serán gestionadas del mismo modo para cada uno de los valores. Del mismo modo, sus respuestas también se agruparán:

const fetchA = fetch('https://api.github.com/users/edulazaro');
const fetchB = fetch('https://api.github.com/users/taylorotwell');

Promise.all([fetchA, fetchB])
  .then(response => {
    console.log('Array de resultados', response);
  })
  .catch(error => {
    console.error(error);
  });

Además, en este caso también está disponible el método race, que se ejecutará tan pronto como una de las promesas se resuelva, ejecutando el callback asociado una única vez, con el resultado de la primera promesa resuelta.

En el siguiente ejemplo, se ejecutará primero la promesa promesaB, dado que el tiempo establecido en el setTimeout() que se ejecuta en su interior es menor que el establecido en la promesa promesaA:

const promesaA = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000, 'A');
});
const promesaB = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, 'B');
});

Promise.race([promesaA, promesaB]).then(resultado => {
  console.log(resultado); // 'B'
});

En lugar de usar el timeout, podríamos usar también la API Fetch o cualquier otra.

Solución de problemas con promesas

Un error que suele darse habitualmente con las promesas es el siguiente:

Uncaught TypeError: undefined is not a promise

Si ves este error en la consola, asegúrate de que has creado la promesa usando su constructor, con new Promise(), y que no has usado Promise()  a secas.

Alternativas a las promesas

Las promesas resultan muy útiles a la hora de ejecutar código de forma asíncrona, incluyendo muchas mejoras con respecto al uso de funciones callback. El uso de then facilita mucho la gestión de las respuestas y evita que el código sea difícil de leer, evitando así el famoso callback hell.

Sin embargo, muchos desarrolladores prefieren usar un formato de código síncrono a la hora de escribir código asíncrono. Por ello, en la versión ES7 de JavaScript, que usa el estándar ECMAScript 2016, se introdujo el uso de las sentencias async y await, facilitando así el uso de las promesas. A continuación tienes un tutorial en el que se explica el uso de async y await, y otro en el que se explica cómo funcionan los callbacks.:

Si quieres ver en detalle cómo usas las promesas con la API Fetch, consulta el siguiente tutorial, en donde explico qué es y cómo se utiliza la API Fetch de JavaScript.

Y esto ha sido todo.


Avatar de Edu Lazaro

Edu Lázaro: Ingeniero técnico en informática, actualmente trabajo como desarrollador web y programador de videojuegos.

👋 Hola! Soy Edu, me encanta crear cosas y he redactado esta guía. Si te ha resultado útil, el mayor favor que me podrías hacer es el de compatirla en Twitter 😊

Si quieres conocer mis proyectos, sígueme en Twitter.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

“- Hey, Doc. No tenemos suficiente carretera para ir a 140/h km. - ¿Carretera? A donde vamos, no necesitaremos carreteras.”