MarioGiron.com

Dominando las Promesas en Javascript: La guía definitiva

5th January 2024
article
Última actualización:17th June 2024
9 Minutos
1604 Palabras

Cuando aprendes a desarrollar con cualquier lenguaje de programación hay ciertos aspectos que son fundamentales y que no debes dejar escapar. En el caso de Javascript y viendo su evolución durante los últimos años debemos controlar cada uno de los detalles relacionados con las promesas (Promise).

En este artículo voy a tratar de desgranar las herramientas básicas para que la creación y resolución de promesas no se convierta en un dolor de cabeza para tí.

¿Qué es una promesa?

Uno de los conceptos fundamentales para el desarrollo de aplicaciones moderno es la asincronía.

Aparte de la creación de la interfaz de usuario, cuando estamos desarrollando para la web o para dispositivos móviles, necesitamos lanzar muchas acciones en paralelo. Dichas acciones consumen recursos (más o menos dependiendo de la cantidad de trabajo que tengan que realizar), los cuales puede ser que bloqueen otras acciones que se estén ejecutando al mismo tiempo.

De ahí que el concepto de asincronía sea de vital importancia. Debemos ser capaces como desarrolladores de lanzar muchas acciones a la vez y que ninguna de ellas se bloquee entre si.

¿Cómo nos ayudan las promesas en este trabajo asíncrono?

No es un concepto complicado, es más, te lo explico con un clásico del cine de tarde de Antena 3:

El pequeño Jimmy tiene un partido muy importante este viernes. Su equipo, después de un gran esfuerzo colectivo, se juega el pase para los campeonatos regionales.

El niño, ilusionado por el caracter épico del encuentro y reclamando cierta atención paterna, le ha preguntado a su padre si iba a sacar un hueco en su agenda de broker de Wall Street.

Su padre, ajeno a la importancia que tiene el choque para el pequeño y además, mostrando el poco espíritu familiar que le queda después de hacer más horas extra q Oliver Atom de camino a la portería, le ha prometido que asistirá al encuentro, cueste lo que cueste.

La promesa está hecha, ahora es tarea del pobre Jimmy, el cual, el día del partido estará atento a la resolución de la promesa de su padre.

¿Verá aparecer a su progenitor a través de las escaleras del estadio obteniendo así un final feliz y poco traumático para el niño?.

Por contra, ¿tendrá que transcurrir todo el partido sin su padre y seguramente desde el banquillo?

default

Ahora que ya hemos recordado cómo se traumatizan a los niños en Estados Unidos, podemos extrapolarlo al mundo de las promesas en Javascript.

AL igual que le pasaba al padre de Jimmy, mediante una promesa podemos ejecutar cualquier acción que va a suceder en un futuro y de la cual desconocemos su resolución.

La resolución de la promesa en el futuro puede ser positiva (el padre aparece y llegan a las regionales) o puede ser una resolución negativa (Jimmy se queda con el trauma y deja el deporte).

Esto es una ventaja para resolver cualquier tipo de acción. Y, aunque podemos ejecutar cualquier operación dentro de nuestras promesas lo más habitual es utilizarlas para todas aquellas acciones que conlleven una carga de trabajo adicional ya sea por el uso de recursos o por el tiempo que tarden en resolverse.

Uno de los casos más comunes para el uso de promesas es a la hora de lanzar una petición externa contra una API o servicio.

El uso de promesas nos asegura que nuestras operaciones complejas se van a resolver de manera asíncrona y además nos proporciona una serie de herramientas para obtener la resolución positiva de la acción o la resolución negativa de la misma.

default

Implementación de Promesas

En nuestro día a día desarrollando vamos a ser grandes consumidores de promesas. Los métodos que utilicemos de las diferentes librerías incluidas en nuestro stack de confianza tienen muchas posibilidades de retornar un objeto Promise como respuesta así que únicamente tendremos que consumir dicha promesa a través de los diferentes métodos disponibles.

No está de más conocer cómo se crea una promesa y cómo se maneja la acción a llevar a cabo de manera interna para tener controlado todo el flujo de trabajo.

¿Cómo se crea una promesa?

Lo primero que necesitamos saber es que los objetos creados a partir de la clase Promise tienen 3 estados:

  • Pendiente (pending). En este caso la acción determinada por la promesa todavía no ha llegado a su conclusión.

  • Completa (fullfilled). Se ha resuelto la promesa y además de manera positiva.

  • Rechazada (rejected). Se ha resuelto la promesa igual que antes pero esta vez de manera negativa.

Si necesitamos crear una promesa utilizamos el constructor de la clase Promise

1
const prom = new Promise(
2
(resolve, reject)=>{
3
// Lanzar las acciones a resolver dentro de la promesa
4
if(/* se resuelve correctamente */){
5
resolve();
6
}else{
7
reject();
8
}
9
}
10
);

El constructor recibe como parámetro una función anónima con dos parámetros a su vez. El primer parámetro (resolve) es la instancia de la función __que ejecutaremos cuando queramos indicar que la resolución de la acción ha terminado de manera positiva. El segundo parámetro (reject) será ejecutado cuando queramos expresar la resolución negativa de la promesa.

Como podemos ver, el constructor nos devuelve un objeto de tipo Promise por lo que podríamos retornarlo como resultado de una función, pasarlo como parámetro para utilizarlo dentro de otra función o simplemente resolverlo como un objeto independiente.

¿Cómo se consume una promesa?

Una vez hemos recuperado el objeto Promise con el que vamos a trabajar disponemos de dos opciones para poder consumir el valor positivo o el valor negativo de la resolución de dicha promesa.

Resolución mediante callback

Disponemos de los métodos then y catch para poder ejecutar ciertas acciones en la resolución positiva de la promesa y en la negativa respectivamente.

Si al crear la promesa ejecutábamos resolve y reject, como parámetros de los métodos then y catch vamos a describir la definición de los métodos asociados a los primeros.

Simplificándolo mucho:

  • then es la definición de resolve
  • catch es la definición de reject

La promesa anterior la podríamos resolver de la siguiente manera

1
prom
2
.then(()=>{
3
// Acciones a ejecutar cuando se resuelva de manera positiva
4
})
5
.catch(()=>{
6
// Acciones a ejecutar cuando se resuelva de manera negativa
7
})

Lo bueno de este tipo de resoluciones es que sabemos con total seguridad que las sentencias que coloquemos dentro de la definición de función del método then se van a ejecutar cuando todo haya ido correctamente, mientras que lo que desarrollemos en el catch sabemos que se va a ejecutar cuando la resolución de la promesa haya ido mal.

Uno de los métodos más característicos en Javascript que devuelve una promesa en su ejecución es fetch. Este método nos permite lanzar una petición HTTP sobre la url que especifiquemos como parámetro.

Un ejemplo de ejecución podría ser el siguiente:

1
fetch('https://peticiones.online/api/users')
2
.then((response)=>{
3
// La variable response contiene toda la información relativa a la petición que hemos lanzado.
4
5
// Mediante el método json aplicado a la respuesta podemos recuperar la parte del body. Este método también retorna una promesa
6
response.json()
7
.then((data)=>{
8
console.log(data);
9
})
10
.catch((error)=>{
11
console.log('Error en la obtención de los datos');
12
});
13
})
14
.catch((error)=>{
15
console.log('Error en la petición', error);
1 collapsed line
16
});

En este caso hemos aplicado al pie de la letra la resolución de promesas, pero el código obtenido no es demasiado legible ni por supuesto mantenible en el tiempo.

Para poder solucionar el caso anterior debemos tener en cuenta que si dentro de la función situada como parámetro del método then retornamos otra promesa, la resolución de la misma se puede realizar en otra llamada al método then encadenada.

Modificamos el ejemplo anterior.

1
fetch('https://peticiones.online/api/users')
2
.then((response)=>{
3
return response.json();
4
})
5
.then((data)=> {
6
console.log(data);
7
})
8
.catch(error => {
9
console.log('Ha ocurrido un error', error);
10
});
11
12
// Más simple
13
fetch('https://peticiones.online/api/users')
14
.then(response => response.json())
15
.then(data => console.log(data))
1 collapsed line
16
.catch(error => console.log('Ha ocurrido un error', error));

De esta manera simplificamos mucho el código y resulta más sencillo trabajar con acciones encadenadas.

De todas maneras, si tenemos que concatenar muchas acciones y en cada una de ellas tenemos promesas involucradas, el nivel de anidación que provocamos puede no ser el adecuado. Es por eso que disponemos de otra sintáxis para poder suavizar el impacto que tiene lanzar varias promesas dependientes entre sí.

Resolución con async/await

Pongamos un ejemplo en el que la ejecución de una promesa dependa de la respuesta de la anterior

1
fetch('https://peticiones.online/api/products')
2
.then(response => response.json())
3
.then(data => {
4
fetch(`https://peticiones.online/api/products/${data[0].id}`)
5
.then(responseById => responseById.json())
6
.then(dataById => console.log(dataById))
7
});

Como vemos en el ejemplo, cada vez que tenemos resolución de promesas dependientes vamos incluyendo un nivel de anidación extra y vamos perdiendo a su vez legibilidad en nuestro código.

Para poder evitar esto disponemos de una sintáxis totalmente equivalente a la anterior pero con mucha más flexibilidad a la hora de establecer nuestro código.

Para poder aplicarla debemos seguir estas simples reglas:

  • Colocaremos la palabra reservada await delante de la promesa.
  • Decorando el ámbito de función donde estemos recuperando la promesa colocamos la palabra reservada async.
  • El resultado positivo de la resolución de la promesa lo asignamos a una variable delante de la ejecución.
  • El resultado negativo de la resolución de la promesa lo obtenemos mediante el uso del bloque try-catch.

Sobre el ejemplo anterior vemos cómo podemos modificarlo para usar async-await.

1
async function getData(){
2
try{
3
const response = await fetch('https://peticiones.online/api/products');
4
const data = await response.json();
5
const responseById = await fetch(`https://peticiones.online/api/products/${data[0].id}`);
6
const dataById = responseById.json();
7
console.log(dataById);
8
}catch(error){
9
console.log(error);
10
}
11
}

Mediante esta sintaxis obtenemos un código muy limpio, fácil de leer y de actualizar.

Conclusiones

Estos son los principios básicos para empezar a trabajar con promesas. Si profundizamos más obsrvaremos la cantidad de métodos que tienen relacionados y las diferentes posibilidades que tenemos para gestionarlas, sobre todo cuando trabajamos con múltiples promesas en paralelo.

En el desarrollo Javascript actual creo que son imprescindibles ya que la mayoría de las librerías o frameworks a los que nos enfrentemos ya las implementarán por defecto y seguirá siendo así en un futuro próximo.

Así que ya es hora de ponerse las pilas con las promesas y no quedarnos como el pobre Jimmy con nuestras ilusiones frustradas. 😜

default

Título del artículo:Dominando las Promesas en Javascript: La guía definitiva
Autor del artículo:Mario Girón
Tiempo de lanzamiento:5th January 2024