JavaScript — Callbacks y Promises

Mauricio Garcia
14 min readJan 24, 2020

--

Temario

  • Funciones callback (síncrono, asíncrono, ventajas y desventajas)
  • Callback Hell (The Pyramid of Doom)
  • Promesa (sintaxis y sus estados)
  • Cambiando el estado de una promesa (resolve, reject)
  • Escuchando cuando cambia el estado de una promesa (then, catch, all, síncrono)
  • Ventajas y desventajas de usar promesas
  • Fetch

i. Funciones callback

Antes de meternos de lleno con los callback, recordemos un poco las funciones[ref]:

En el ejemplo anterior podemos observar que hemos creado una función sum, donde invocamos la función sum con los parámetros 4, 6.

En la story de funciones[ref] ya hemos visto un poco los callbacks o funciones de orden superior; entonces, no debemos olvidar lo siguiente:

Las funciones que llaman a otras funciones o que devuelven funciones, se conocen como funciones de orden superior, muchas veces también recibe el nombre de función de devolución de llamada (callbacks).

Donde podemos tener dos tipos: Síncrono y Asíncrono

a — síncrono

Síncrono: Es cuando se ejecuta una operación de entrada/salida de manera secuencial, por lo que debemos de esperar a que se complete para poder procesar el resultado.

Veamos el siguiente ejemplo:

Ejemplo 1.0: Imaginemos que somos fanáticos del ciclismo y quiero felicitar a Eddy Merckx[ref] y que él a su vez me dé las gracias por las palabras que le he dicho; entonces, nuestro ejemplo quedaría así:

Ejemplo 1.1: Al ser un ciclista muy popular, estoy seguro que no soy el único que lo busca para demostrarle lo bueno que es; entonces, veamos la siguiente imagen:

El código quedaría de la siguiente manera:

Hemos creado 3 personas (constantes) donde cada uno invoca al ciclista (cyclist) donde él ha agradecido a cada uno de ellos.

Podemos observar que no importa quien invoque a la función, SIEMPRE va a regresar un valor.

Ejemplo 1.2: Nuestro ciclista famoso está entrenando para una gran carrera, por lo que no va a ser posible atender a sus fans de manera directa, y la única forma de hacerlo es mediante su agente, hasta aquí todo bien… pero resulta que el agente maneja a más personas, por lo que es necesario decirle a quien va dirigido nuestro saludo.

Entonces, para que nuestro mensaje llegue, debe ser por medio del agente, donde espera que le digamos nuestro nombre, un mensaje y a quién va dirigido.

Veamos el código completo:

Spoiler: El ejemplo anterior es un patrón (Veremos más a fondo en patrones de diseño **).

Recordemos que las funciones podemos pasarle cualquier tipo de dato (primitivo y complejos) como argumento, y entre esos tipos se encuentra una función, por lo que, podemos mandar una función que a su vez la invoque.

Resumiendo… los callbacks son funciones que se pasan como parámetros a otras funciones y se invocan dentro de éstas, veamos otro ejemplo:

Hemos declarado una función llamada foo, que va a recibir como parámetro otra función callback (se puede poner el nombre que uno quiera), cuando ejecutamos nuestra función foo, mandamos como parámetro una función anónima, que esta va a imprimir en consola Hello world!!

Veamos un ejemplo muy común que se usa para explicar el callback:

La función sum, recibe 3 parámetros, los 2 primeros son los valores numéricos y el tercero es el callback, donde va a ser el encargado de regresar el resultado de la suma.

De hecho el uso de las funciones tipo callback las usamos más de lo que pensamos:

b — asíncrono

Asíncrono: Ejecutar una operación, donde la finalización es notificada de manera diferida.

Hasta el momento, hemos visto los callback de forma síncrona, pero también se usa mucho en la parte asíncrona, es muy común cuando pedimos datos a una API, y esperamos que nos los devuelva, para poder usar en nuestra aplicación.

Para nuestro siguiente ejemplo vamos a usar XMLHttpRequest de JavaScript, donde el código base es el siguiente:

Para el siguiente ejemplo vamos a utilizar la API de themoviedb[ref] (hay que darse de alta en la página para obtener una key y poder usarla):

Resultado:

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Bien, una vez que ya hemos entendido cómo funciona, continuemos con los callbacks; para eso vamos a envolver el código anterior en una función, donde:

  • Reciba el tipo de request (tipo string)
  • La url de la API a consumir (tipo string)
  • Función donde nos va a retornar cuando regrese los datos. (tipo función callback)

Entonces, quedaría de la siguiente manera:

Veamos un ejemplo de cómo se ejecuta la función:

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Y así, es cómo hacemos uso del callback para solicitudes asíncronas.

c — Ventajas y desventajas de usar callbacks

Ventajas:

  • Los callbacks son simples, pasas la función que quieras que se ejecute después.
  • Los callbacks corren donde sea (no requiere de un transpilador o polyfill).

Desventajas:

  • Las llamadas anidadas CallBack Hell (en el siguiente tema explicamos a profundidad).
  • Flujo poco intuitivo, los callback requiere recorrer las funciones para comprenderlas.
  • Para el manejo de errores puede ser un dolor de cabeza.
  • El control del callback, le dejamos a un tercero el control del retorno, asumiendo que regresará en el momento adecuado con los datos adecuados (Inversión de control [ref])

ii. Callback Hell (The Pyramid of Doom)

Veamos un ejemplo: La API de themoviedb tiene diferentes servicios para obtener ciertos datos: película, casting, créditos, imágenes, películas similares, etc; entonces, para nuestro ejemplo vamos ocupar los siguientes: película, casting, videos…

Quizás como primera solución, lo primero que se nos ocurre es:

No está mal la solución, pero, ¿qué pasaría si queremos guardar en un JSON todos los valores, y hasta que tengamos las 3 peticiones, es cuando vamos a mostrarlo en nuestra página, quizás se nos ocurre anidar una función dentro de otra función:

Nota: Como buena práctica creado variables reutilizables

Ver ejemplo completo:

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Que pasaria si en vez de 3 servicios necesitamos consumir 8 o más, creo que nuestro código comenzaría a verse algo así:

Aquí es donde empezamos a tener el famoso problema llamado callback hell, ya que como personas pensar de forma anidada no es natural, al momento de programar, es donde más se comenten errores; quizás el siguiente paso, es modularizar, para sea hacer más legible el código:

Quizás los nombres de las funciones nos ayuden un poco a entender que está sucediendo, realmente, no hemos hecho grandes mejoras, solo hemos puesto un parche a nuestro problema.

El ejemplo de arriba es muy sencillo, pero, ahora imaginemos que dentro de cada respuesta necesitamos iterar, hacer un bucle de peticiones, o cualquier otra cosa compleja; creo que nuestro código va a hacer un dolor de cabeza…

No todo está perdido, por fortuna podemos “solucionarlo” con Promesas…

iii. Promesa

Creo que todos en algún momento hemos tomado un taxi ¿no?, sabemos que debemos ir a una esquina y esperar a que uno de ellos pase, si tenemos suerte (al menos en México), se pare y nos lleve a nuestro destino. Luego como ocurre naturalmente salieron los taxis privados, donde, por medio de un mensaje podías pedirlo, la desventaja es que luego nos mandaban mensajes de publicidad, promociones… ¿les suena?, este último, es un excelente ejemplo de un callback, nosotros mandamos un mensaje para solicitar un servicio, esperando, un mensaje de que ya se encuentran donde lo pedimos (qué es lo que nosotros esperamos), pero ellos, se adueñan de nuestro número y nos pueden mandar publicidad, por lo que terminamos perdiendo el control…

Por fortuna siguió evolucionando, y fue cuando llegaron las aplicaciones (Uber, Cabify, Didi, etc), donde nos dan el control absoluto a nosotros. Si nunca has usado una aplicación, la idea es simple, nosotros abrimos la aplicación, le indicamos dónde nos va a recoger y a dónde nos va a dejar; lo magnífico de esto es que podemos realizar otras tareas en lo que llega, y es la aplicación la que nos avisa cuando ha llegado el conductor…

La aplicación siempre va a tener estos tres estados: pendiente, cumplido, cancelado.

  • Pendiente (pending): Cuando pedimos un servicio y va en camino
  • Cumplido (resolve): Cuando ha llegado por nosotros
  • Cancelado (reject): Cuando algo sale mal, quizás el socio lo cancelo, o no hay disponibles en ese momento.

Entonces, nosotros tenemos el control absoluto, ya que cuando se ha cumplido si queremos podemos bajar y subirnos o podemos ignorarlo si queremos, si se ha cancelado, quizás nos ponga de malas pero bah!, podemos pedir otro servicio en otra aplicación, y si está pendiente, podemos hacer otras cosas y no hay ningún problema; por lo que la aplicación nos hace una promesa de que van a ir por nosotros y nos van a proveer un servicio. Quizás la analogía no sea aplicable al 100%, pero nos da una idea de cómo funciona.

a — Sintaxis

Ahora sí, veamos la sintaxis para crear una promesa:

La función constructora Promise, tiene solo un argumento, que es una función (callback) que recibe el nombre de ejecutor, cuando se crea una promesa el ejecutor se ejecuta automáticamente; siguiendo nuestro ejemplo: aquí es donde pedimos el servicio a la aplicación.

b — Estados de una promesa

Podemos observar que la función tiene dos argumentos resolve y reject, estos son callbacks que nos proporciona el propio JavaScript, y que nos van a permitir cambiar el estado de la promesa; siguiendo nuestro ejemplo: cuando la aplicación nos regresa un resultado, debería llamar a uno de los dos callbacks (resolve, reject).

iv. Cambiando el estado de una promesa

Veamos cómo podemos cambiar a uno de estos dos estados…

a — Resolve

En el siguiente ejemplo, vamos a utilizar el setTimeout[ref] para esperar 3 segundos y luego invocamos resolve:

Nuestro código:

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Podemos observar que:

  • El ejecutor se ejecuta automáticamente, creando a la promesa el estado pending.
  • Después de 3 segundos, el ejecutor invoca la función resolve, donde le pasa el parámetro tipo string done”, esto hace que el estado de la promesa cambie a fulfilled.

b — Rejected

Ahora en el siguiente ejemplo, vamos a utilizar el setTimeout[ref] para esperar 3 segundos y luego invocamos reject.

Nuestro código:

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Podemos observar que:

  • El ejecutor se ejecuta automáticamente, creando a la promesa el estado pending.
  • Después de 3 segundos, el ejecutor invoca la función reject, donde le pasa el parámetro tipo string Error!!!!”, esto hace que el estado de la promesa cambie a rejected.

v. Escuchando cuando cambia el estado de una promesa

Ya sabemos como crear una promesa, también sabemos cómo invocar el cambio de estado, pero aún no hemos visto cómo escuchar esos estados.

La promesa puede invocar dos métodos: .then y .catch.

  • Cuando el estado de la promesa cambia a fulfilled (resolved), .then será invocado
  • Cuando el estado de la promesa cambia a rejected, .catch será invocado

Veamos ejemplos de cada uno de ellos…

a — then

En el siguiente ejemplo, vamos a utilizar el setTimeout[ref] para esperar 3 segundos, luego invocamos resolve, y escuchamos el cambio de estado en .then

Nuestro código:

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Podemos observar que:

  • El ejecutor se ejecuta automáticamente, creando a la promesa el estado pending.
  • Después de 3 segundos, el ejecutor invoca la función resolve, donde le pasa el parámetro tipo string done”, esto hace que el estado de la promesa cambie a fulfilled.
  • Cuando cambia el estado, el motor de JavaScript invoca la función .then, donde sale en consola Success! done

b — catch

En el siguiente ejemplo, vamos a utilizar el setTimeout[ref] para esperar 3 segundos, luego invocamos reject, y escuchamos el cambio de estado en .catch

Nuestro código:

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Podemos observar que:

  • El ejecutor se ejecuta automáticamente, creando a la promesa el estado pending.
  • Después de 3 segundos, el ejecutor invoca la función reject, donde le pasa el parámetro tipo string Error!”, esto hace que el estado de la promesa cambie a rejected.
  • Cuando cambia el estado, el motor de JavaScript invoca la función .catch, donde sale en consola Fail!! Error!

c — Promise.all

El método Promise.all(iterable) devuelve una promesa que termina correctamente cuando todas las promesas en el argumento iterable han sido concluidas con éxito, o rechazada si alguna de ellas es rechazada.[ref]

Imaginemos que tenemos 3 promesas:

Queremos que cuando las 3 estén (sin importar el orden en que son resueltas), podamos obtener cada uno de los datos; entonces el código quedaría de la siguiente forma:

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Podemos observar, que hasta que las 3 promesas concluyen de manera satisfactoria, es cuando ejecuta la función .then de Promise.all:

vi. Ventajas y desventajas de usar promesas

Ventajas:

  • Mejora la legibilidad del código.
  • Mejor manejo de operaciones asincrónicas.
  • Mejor definición de flujo de control en lógica asincrónica.
  • Mejor manejo de errores.
  • Control completo de nuestro lado.
  • Solo puede haber un único resultado o un error. Todas las llamadas adicionales de resolve y reject se ignoran:

Desventajas:

  • No hay forma de romper una petición de manera manual
  • A pesar de que son asíncronas se hace de forma concurrente y no paralela [ref]

Hasta aquí hemos comprendido cómo crear una promesa (new Promise), invocar el cambio de estado (resolve, reject), y escuchar los estados (then, catch).

vii. Fetch

¿Recuerdan el ejemplo que hicimos con callback de API de themoviedb?:

Vamos a actualizar nuestro ejemplo a promesas, para eso, vamos a utilizar la API Fetch[ref].

La API Fetch proporciona una interfaz para recuperar recursos (incluso a través de la red). Resultará familiar a cualquiera que haya usado XMLHttpRequest, pero la nueva API ofrece un conjunto de características más potente y flexible.[ref]

Una de las grandes ventajas de usar Fetch API es de que usa Promises, otra ventaja es que evita el callback Hell y evita el código pesado proporciona XMLHttpRequest (XHR).

La sintaxis es la siguiente:

¿Les suena la sintaxis?, así es!!! es una Promise!!

El método fetch() toma un argumento obligatorio, que es la ruta de acceso al recurso que desea recuperar (en caso de nuestro ejemplo es examples/example.json). Devuelve una Promise que resuelve en Response, sea o no correcta. También puede pasar opcionalmente un objeto de opciones init como segundo argumento[ref].

Cuando se resuelve la promesa, se pasa la respuesta a .then. Si la solicitud no se completa, .catch se hace cargo y se pasa el error correspondiente.

El objeto response representa la respuesta a una solicitud, este contiene el recurso solicitado, propiedades y métodos que son de gran utilidad; por ejemplo, response.ok, response.status, y response.statusText, donde nos van a servir para evaluar el estado de la respuesta.

Evaluar la respuesta es muy importante, ya que solo queremos que se resuelvan las peticiones que están dentro de 200–299, y para todas las demás deben rechazar la petición, entonces nuestro código queda de la siguiente manera:

Ahora ¿cómo obtenemos los datos?, el mismo response tiene métodos adecuados para obtener los datos[ref].

  • response.json() — Devuelve una promesa que se resuelve con el resultado de analizar el texto del cuerpo como JSON.
  • response.text() — Devuelve una promesa que se resuelve con un USVString (texto)[ref]. La respuesta siempre se decodifica usando UTF-8.
  • response.arrayBuffer() — Devuelve una promesa que se resuelve con un ArrayBuffer.
  • response.blob() — Devuelve una promesa que se resuelve con a Blob.
  • Body.formData() — Devuelve una promesa que se resuelve con un objeto FormData.

Dependiendo el tipo de dato que vamos a recibir, es el método que vamos a ocupar, para el caso del ejemplo vamos a necesitar response.json()

Por último, este código será más limpio y fácil de entender si se abstrae en funciones:

Ahora sí adaptemos nuestro primer ejemplo la API Fetch:

Nuestro código:

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Vamos a envolver el código anterior en una función, donde va a recibir un parámetro:

  • La url de la API a consumir (tipo string)

Entonces, quedaría de la siguiente manera:

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Podemos observar que hemos sacado el .then(onSuccess) y .catch(onError), veremos el por qué hemos hecho esto…

Ahora el siguiente paso es poder encadenar más de una petición, lo primero que hay que hacer es crear cada una de las peticiones :

Después ejecutamos:

Sencillo ¿no?, lo que hace el código es lo siguiente:

  • Ejecutamos la función getMovie, donde internamente ejecuta fecthJSON
  • Cuando se resuelve, ejecuta la función getVideo, donde internamente ejecuta fecthJSON
  • Cuando se resuelve, ejecuta la función onSuccess, donde imprimimos en consola el resultado

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Oh espera!! ¿y los datos?, ¿como persistimos los datos?, hagamos unos cambios en nuestras funciones:

Veamos el código en tiempo real (no olvides darle al botón verde de play):

Si ejecutamos el código podemos observar que ya se persisten los datos:

Reto: Hacer que las 3 peticiones se ejecuten de manera asíncrona con Promise.all

Solución:

Nota: No olvides que a pesar de las promesas que son asíncronas, JavaScript las maneja de forma concurrente y no paralela.

Para comprender el enunciado anterior te recomiendo leer:

- JavaScript — Cómo funciona el Runtime Environment — JRE)[ref]
- Secuencia, concurrencia y paralelismo [ref]

En la siguiente entrega vamos a ver JavaScript — ES6 (Parte II)

La entrega pasada vimos JavaScript — Cómo funciona el Runtime Environment — JRE)

--

--