YOU DON’T KNOW JS – 05: ASYNC & PERFOMANCE en castellano

Bueno, finalmente he terminado con el que quizás sea uno de los libros de la serie más importantes para los desarrolladores con un buen dominio de Javascript pero que necesitan ese extra de control que proporciona un buen código asíncrono. Cuando digo a buen código no sólo me refiero a un código limpio, entendible y carente de errores, si no a un código eficiente en cuanto al rendimiento.

Hoy en día Javascript en sus nuevas implementaciones nos provee de las promesas y de otras herramientas para manejar la asincronía. Pero si no se entienden bien, podemos obtener de manera muy fácil una aplicación con un rendimiento muy pobre que afectará a la experiencia de usuario de manera muy significativa. Es por ello que este libro resulta tan imprescindible. El libro incluye tres apéndices que he decidido publicar a parte del grueso del libro. No solo por la complejidad y tamaño del propio libro, sino porque cada apéndice merece también una lectura profunda y porque resultará en un más fácil manejo de la documentación. De ellas si he querido incluir al apéndice C de agradecimientos que encontraréis al final, si el autor ha de agradecerles a esas personas su apoyo o colaboración, nosotros tambien.

Bueno, espero que os guste y no necesitéis demasiadas aspirinas para terminarlo. 😀

You Don’t Know JS: Async & Performance


Prólogo


A través de los años, mi empleador ha confiado en mí lo suficiente como para realizar entrevistas. Si estamos buscando a alguien con habilidades en JavaScript, mi primera línea de preguntas… en realidad eso no es verdad, primero compruebo si el candidato necesita ir al baño y/o una bebida, porque la comodidad es importante, pero una vez que haya superado la parte de la toma de fluido del candidato, me puse a determinar si el candidato conoce JavaScript, o simplemente jQuery.

No es que haya nada malo con JQuery. Te permite hacer muchas cosas sin conocer realmente JavaScript, y esa es una característica, no un fallo. Pero si el trabajo requiere habilidades avanzadas en el rendimiento y mantenimiento de JavaScript, necesita alguien que sepa cómo se arman bibliotecas como jQuery. Usted necesita ser capaz de aprovechar el núcleo de JavaScript de la misma manera que lo hace jQuery.

Si quiero obtener una imagen de la habilidad general de JavaScript de alguien, estoy más interesado en lo que hacen de los cierres (ya has leído ese libro de esta serie, ¿verdad?) y cómo sacar el máximo partido de la asincronicidad, lo que nos lleva a este libro.

Para empezar, se le llevará a través de las llamadas, el pan y la mantequilla dela programación asíncrona. Por supuesto, el pan con mantequilla no es una comida particularmente satisfactoria, pero el próximo plato está lleno de sabrosas promesas.

Si no conoces las promesas, ahora es el momento de aprender. Las promesas son ahora la forma oficial de proporcionar valores de retorno de asíncronos tanto en JavaScript como en el DOM. Todas las futuras APIs de DOM asíncronas las usarán, muchas ya lo hacen, ¡así que esté preparado! En el momento de escribir este artículo, Promises ya ha sido enviado en la mayoría de los principales navegadores, con IE pronto. Cuando haya terminado, espero que haya dejado espacio para el próximo curso, Generadores.

Los generadores se colaron en versiones estables de Chrome y Firefox sin demasiada pompa ni ceremonia, porque, francamente, son más complicados de lo que son interesantes. O, eso es lo que pensé hasta que los vi combinados con promesas. Allí, se convierten en una herramienta importante para la legibilidad y el mantenimiento.

Para el postre, bueno, no estropearé la sorpresa, pero prepárate para mirar el futuro de JavaScript!y las características que le dan cada vez más control sobre la concurrencia y asincronía. ¡Bueno, ya no te bloquearé más el disfrute del libro con el espectáculo! Si ya has leído parte del libro antes de leer este Prólogo, ¡date 10 puntos asíncronos! ¡Te los mereces!

Jake Archibald
jakearchibald. com, @jaffathecake
Defensor del desarrollador en Google Chrome

Prefacio


Estoy seguro que lo notaste, pero «JS» en el título de la serie de libros no es una abreviatura de las palabras usadas para maldecir JavaScript, aunque maldecir por las rarezas del lenguaje es algo con lo que probablemente todos nos podemos identificar!

Desde los primeros días de la web, JavaScript ha sido una tecnología fundamental que impulsa la experiencia interactiva en torno al contenido que consumimos. Mientras que las pistas de ratón parpadeantes y las molestas indicaciones emergentes pueden estar donde comenzó JavaScript, casi dos décadas más tarde, la tecnología y la capacidad de JavaScript ha crecido muchos órdenes de magnitud, y pocos dudan de su importancia en el corazón de la plataforma de software más ampliamente disponible del mundo: la web.

Pero como lenguaje, ha sido objeto de constantes críticas, en parte debido a su patrimonio, pero sobre todo a su filosofía de diseño. Incluso el nombre evoca, como dijo una vez Brendan Eich, el estatus de «hermano menor tonto» junto a su hermano mayor más maduro «Java». Pero el nombre no es más que un accidente de la política y el marketing. Los dos lenguajes son muy diferentes en muchos aspectos importantes. «JavaScript» es tan relacionado con «Java» como «Carnival» es con «Car».

Debido a que JavaScript toma prestados conceptos y lenguajes de sintaxis de varios lenguajes, incluyendo orgullosas raíces procedimentales estilo C, así como sutiles y menos obvias raíces funcionales estilo Scheme/Lisp, es extremadamente accesible a una amplia audiencia de desarrolladores, incluso aquellos con poca o ninguna experiencia de programación. El «Hello World» de JavaScript es tan simple que el lenguaje es atractivo y fácil de usar en una exposición temprana.

Mientras que JavaScript es quizás uno de los lenguajes más fáciles de usar, sus excentricidades hacen que el dominio del lenguaje sea mucho menos común que en muchos otros lenguajes. Cuando se necesita un conocimiento bastante profundo de un lenguaje como C o C++ para escribir un programa a gran escala, la producción a gran escala de JavaScript puede, y a menudo lo hace, apenas araña la superficie de lo que el lenguaje puede hacer.

Los conceptos sofisticados que están profundamente enraizados en el lenguaje tienden a aparecer en vez de ello de maneras aparentemente simplistas, como pasar funciones como llamadas de retorno, lo que anima al desarrollador de JavaScript a usar el lenguaje tal como es y no preocuparse demasiado por lo que está pasando bajo el capó.

Simultáneamente, es un lenguaje sencillo y fácil de usar que tiene un amplio atractivo, y una colección compleja y matizada de mecánica del lenguaje que, sin un estudio cuidadoso, eludirá la comprensión verdadera incluso para los desarrolladores de JavaScript más experimentados.

Ahí radica la paradoja del JavaScript, el talón de Aquiles del lenguaje, el reto al que nos enfrentamos actualmente. Debido a que JavaScript se puede usar sin entender, la comprensión del lenguaje a menudo no se logra.

Misión

Si en cada punto en el que te encuentras con una sorpresa o frustración en JavaScript, tu respuesta es añadirlo a la lista negra, como algunos están acostumbrados a hacer, pronto serás relegado a un shell desprovisto de la riqueza de JavaScript.

Mientras que este subconjunto ha sido conocido como «Las partes buenas», le imploro, querido lector, que en su lugar lo considere como «Las partes fáciles», «Las partes seguras», o incluso «Las partes incompletas».

Esta serie de libros de JavaScript ofrece un reto contrario: aprender y comprender a fondo todo el JavaScript, incluso y especialmente «Las partes duras».

Aquí, nos enfocamos en la tendencia de los desarrolladores de JS a aprender «lo suficiente» para sobrevivir, sin forzarse a aprender exactamente cómo y por qué el lenguaje se comporta de la manera en que lo hace. Además, evitamos el consejo común de retirarnos cuando el camino se pone difícil.

No estoy contento, tú tampoco deberías estarlo, al detenerme una vez que algo simplemente funciona, y no saber realmente por qué. Te desafío a que recorras ese «camino menos transitado» y aceptes todo lo que JavaScript es y puede hacer. Con ese conocimiento, ninguna técnica, ningún framework, ninguna palabra de moda popular de la semana, estará más allá de su comprensión.

Cada uno de estos libros aborda partes específicas del lenguaje que más comúnmente son malentendidas o subentendidas, y se zambullen muy profundamente y exhaustivamente en ellas. Usted debe alejarse de la lectura con una firme confianza en su comprensión, no sólo de los aspectos teóricos, sino también prácticos «lo que necesita saber».

El JavaScript que usted conoce en este momento es probablemente partes transmitidas a usted por otros que han sido quemados por un entendimiento incompleto. Ese JavaScript no es más que una sombra del verdadero lenguaje. Todavía no conoces realmente JavaScript, pero si buscas en esta serie, lo harás. Sigan leyendo, amigos míos. JavaScript le espera.

Resumen

JavaScript es impresionante. Es fácil aprender parcialmente, y mucho más difícil de aprender completamente (o incluso lo suficiente). Cuando los desarrolladores encuentran confusión, generalmente culpan al lenguaje en vez de su falta de comprensión. Estos libros apuntan a arreglar eso, inspirando un fuerte aprecio por el idioma que ahora puedes y debes conocer profundamente.

Nota: Muchos de los ejemplos de este libro asumen entornos de motor JavaScript modernos (y de gran alcance en el futuro), como ES6. Es posible que algunos códigos no funcionen como se describe si funcionan en motores anteriores (pre-ES6).

Capítulo 1. Asincronía: Ahora y Después

Una de las partes más importantes y a menudo malentendidas de la programación en un lenguaje como JavaScript es cómo expresar y manipular el comportamiento del programa repartido en un período de tiempo.

Esto no se trata sólo de lo que ocurre desde el principio de un bucle for hasta el final de un bucle for, lo que por supuesto lleva algún tiempo (microsegundos a milisegundos) para completarse. Se trata de lo que sucede cuando una parte de su programa se ejecuta ahora y otra parte de su programa se ejecuta más tarde, hay una brecha entre el momento actual y el momento en que su programa no se está ejecutando activamente.

Prácticamente todos los programas no triviales que se han escrito (especialmente en JS) de alguna manera u otra han tenido que manejar esta brecha, ya sea esperando la entrada del usuario, solicitando datos de una base de datos o sistema de archivos, enviando datos a través de la red y esperando una respuesta, o realizando una tarea repetida en un intervalo de tiempo fijo (como animación). En todas estas diversas maneras, su programa tiene que manejar el estado a través de la brecha en el tiempo. Como dicen en Londres (sobre el hueco entre la puerta del metro y el andén): «Cuidado con la brecha».

De hecho, la relación entre las partes actuales y posteriores de su programa está en el corazón de la programación asíncrona.

La programación asíncrona ha estado presente desde el principio de JS, seguramente. Pero la mayoría de los desarrolladores de JS nunca han considerado cuidadosamente cómo y por qué aparece en sus programas, o han explorado otras formas de manejarlo. El enfoque lo suficientemente bueno siempre ha sido la humilde función de devolución («callbacks» a partir de ahora) de llamada. Muchos hasta el día de hoy insisten en que los callbacks son más que suficientes.

Pero a medida que JS continúa creciendo tanto en alcance como en complejidad, para satisfacer las exigencias cada vez mayores de un lenguaje de programación de primera clase que se ejecuta en navegadores y servidores y en todos los dispositivos imaginables entre ellos, los dolores que produce el manejar la asincronía se están volviendo cada vez más paralizantes, y claman por enfoques que sean tanto más capaces como más razonables.

Si bien todo esto puede parecer bastante abstracto en este momento, les aseguro que lo abordaremos de manera más completa y concreta mientras continuamos con este libro. Exploraremos una variedad de técnicas emergentes para sincronizar la programación JavaScript en los próximos capítulos.

Pero antes de que podamos llegar allí, vamos a tener que entender mucho más profundamente qué es la asincronía y cómo funciona en JS.

Un programa en trozos


Usted puede escribir su programa JS en un archivo .js, pero su programa está compuesto casi con seguridad por varios trozos, sólo uno de los cuales se va a ejecutar ahora, y el resto se ejecutará más tarde. La parte unitaria más común es la función.

El problema que la mayoría de los desarrolladores nuevos en JS parecen tener es que más tarde no sucede estrictamente e inmediatamente después de ahora. En otras palabras, las tareas que no pueden completarse ahora, por definición, se van a completar asincrónicamente, y por lo tanto no tendremos un comportamiento de bloqueo como usted podría esperar o desear intuitivamente.

Considere esto:

Probablemente eres consciente de que las peticiones Ajax estándar no se completan sincrónicamente, lo que significa que la función ajax(...) todavía no tiene ningún valor para regresar a la variable de data. Si ajax(...) pudiera bloquear hasta que la respuesta regresara, entonces la asignación data = ... funcionaría bien.

Pero así no es como hacemos Ajax. Hacemos una solicitud asincrónica de Ajax ahora, y no tendremos los resultados hasta más tarde.

La forma más simple (pero definitivamente no sólo, o necesariamente la mejor) de «esperar» desde ahora hasta más tarde es usar una función, comúnmente llamada función de devolución de llamada (callback en inglés):

Advertencia: Puede que hayas oído que es posible hacer peticiones síncronas de Ajax. Si bien eso es técnicamente cierto, nunca debes hacerlo, bajo ninguna circunstancia, porque bloquea la interfaz de usuario del navegador (botones, menús, scrolling, etc.) e impide cualquier interacción con el usuario. Esta es una idea terrible, y siempre debe evitarse.

Antes de que usted proteste en desacuerdo, no, su deseo de evitar el desorden de las llamadas de retorno no es la justificación para el bloqueo, síncrono de Ajax.

Por ejemplo, considere este código:

Hay dos partes de este programa: la que se ejecutará ahora, y la que se ejecutará más tarde. Debería ser bastante obvio lo que son esos dos trozos, pero seamos muy explícitos:

Ahora:

Más tarde:

La parte now corre de inmediato, tan pronto como ejecute su programa. Pero setTimeout(...) también configura un evento (un timeout) para que ocurra más tarde, por lo que el contenido de la función later() se ejecutará más adelante (1.000 milisegundos a partir de ahora).

Cada vez que usted envuelve una porción de código en una función y especifica que debe ser ejecutada en respuesta a algún evento (timer, clic del ratón, respuesta de Ajax, etc.), usted está creando un trozo posterior de su código, y por lo tanto introduciendo asincronía a su programa.

Consola Asíncrona

No hay ninguna especificación o conjunto de requisitos en torno a cómo funcionan los métodos console.* — no son oficialmente parte de JavaScript, sino que son añadidos a JS por el entorno del host (ver el título Tipos y gramática de esta serie de libros).

Por lo tanto, diferentes navegadores y entornos JS hacen lo que les place, lo que a veces puede llevar a comportamientos confusos.

En particular, hay algunos navegadores y algunas condiciones en que console.log (...) no muestra inmediatamente lo que se obtiene. La razón principal por la que esto puede ocurrir es porque la E/S es una parte muy lenta y que bloquea muchos programas (no sólo JS). Por lo tanto, puede funcionar mejor (desde la perspectiva de página/UI) para que un navegador pueda manejar la E/S de console de forma asincrónica en segundo plano, sin que usted ni siquiera sepa que ocurrió.

Un escenario no terriblemente común, pero posible, donde esto podría ser observable (no desde el código mismo, sino desde fuera):

Normalmente esperaríamos que el objeto a sea recogido en el momento exacto de la sentencia console.log(..), imprimiendo algo parecido a { index: 1 }, de tal manera que en la siguiente sentencia, cuando ocurre un a.index++, está modificando algo diferente, o estríctamente despues, a la salida de a.

La mayoría de las veces, el código precedente probablemente producirá una representación de objetos en la consola de sus herramientas de desarrollo que es lo que usted esperaría. Pero es posible que este mismo código pueda ejecutarse en una situación en la que el navegador pensó que era necesario pasar la E/S de la consola al fondo, en cuyo caso es posible que para cuando el objeto esté representado en la consola del navegador, a. index++ ya haya ocurrido, y muestre { índice: 2 }.

Se trata de algo no estático sobre qué condiciones exactamente se pospondrá la E/S de la consola, o incluso si será observable. Sólo ten en cuenta esta posible asincronicidad en la E/S en caso de que te encuentres con problemas en la depuración donde los objetos han sido modificados después de una sentencia console. log(..) y sin embargo veas que aparecen las modificaciones inesperadas.

Nota: Si te encuentras con este escenario raro, la mejor opción es usar puntos de interrupción en tu depurador JS en lugar de depender de la salida de la consola. La siguiente mejor opción sería forzar una «instantánea» del objeto en cuestión serializándolo a una cadena, como con JSON.stringify (...).

Bucle de eventos


Hagamos una afirmación (quizás chocante): a pesar de permitir claramente el código asincrónico JS (como el timeout que acabamos de ver), hasta hace poco (ES6), JavaScript en sí mismo nunca ha tenido ninguna noción directa de asincronía incorporada en él.

¿Qué!? Parece una afirmación loca, ¿no? De hecho, es bastante cierto. El motor JS en sí nunca ha hecho nada más que ejecutar una sola parte de su programa en un momento dado, cuando se le pide.

«Solicitada». ¿Por quien? ¡Esa es la parte importante!

El motor JS no funciona de forma aislada. Se ejecuta dentro de un entorno de host, que es para la mayoría de los desarrolladores el típico navegador web. Durante los últimos años (pero de ninguna manera exclusivamente), JS se ha expandido más allá del navegador a otros entornos, como servidores, a través de cosas como Node.js. De hecho, JavaScript se integra en todo tipo de dispositivos en la actualidad, desde robots hasta bombillas.

Pero el único «hilo conductor» común (que es una broma asincrónica no tan sutil, por si sirve de algo) de todos estos entornos es que tienen un mecanismo en ellos que maneja la ejecución de múltiples trozos de tu programa a lo largo del tiempo, invocando en cada momento el motor JS, llamado «bucle de eventos».

En otras palabras, el motor JS no ha tenido un sentido innato del tiempo, sino que ha sido un entorno de ejecución bajo demanda para cualquier fragmento arbitrario de JS. Es el entorno circundante el que siempre ha programado «eventos» (ejecuciones de código JS).

Así que, por ejemplo, cuando su programa JS hace una solicitud Ajax para obtener algunos datos de un servidor, usted configura el código de «respuesta» en una función (comúnmente llamada «callback»), y el motor JS le dice al entorno host, «Hey, voy a suspender la ejecución por ahora, pero cuando termine con esa solicitud de red, y tenga algunos datos, por favor llame a esta función de nuevo».

El navegador está entonces configurado para escuchar la respuesta de la red, y cuando tiene algo que darle, programa la función dcallback a ejecutar insertándola en el bucle de eventos.

Entonces, ¿cuál es el bucle de eventos?

Conceptualizémoslo primero a través de un pseudocódigo:

Esto es, por supuesto, un pseudocódigo muy simplificado para ilustrar los conceptos. Pero debería ser suficiente para ayudar a obtener una mejor comprensión.

Como puedes ver, hay un bucle en ejecución continua representado por el bucle while, y cada iteración de este bucle se llama «tick». Por cada tiCk, si un evento está esperando en la cola, se quita y se ejecuta. Estos eventos son sus llamadas de retorno de funciones.

Es importante tener en cuenta que setTimeout(..) no pone tu llamada de retorno en la cola del bucle de eventos. Lo que hace es configurar un temporizador; cuando el temporizador expira, el entorno coloca tu llamada de vuelta en el bucle de eventos, de manera que algun tick futuro lo recogerá y lo ejecutará.

¿Qué pasa si ya hay 20 ítems en el bucle de eventos en ese momento? Tu llamada espera. Se pone en línea detrás de las otras — normalmente no hay un camino para evitar la cola y saltar adelante en línea. Esto explica por qué los temporizadores setTimeout(..) pueden no dispararse con una precisión temporal perfecta. Usted está garantizado (aproximadamente hablando) que su devolución de llamada no se disparará antes del intervalo de tiempo que especifique, pero puede ocurrir en ese momento o después, dependiendo del estado de la cola de eventos.

Así que, en otras palabras, su programa generalmente se divide en muchos trozos pequeños, que suceden uno tras otro en la cola del bucle de eventos. Y técnicamente, otros eventos no relacionados directamente con su programa también pueden ser intercalados dentro de la cola.

Nota: Hemos mencionado «hasta hace poco» en relación con como en ES6 se cambia la naturaleza de donde se maneja la cola del bucle de eventos. La mayoría es un tecnicismo formal, pero ES6 ahora especifica cómo funciona el bucle de eventos, lo que significa que técnicamente está dentro del ámbito del motor JS, en lugar de sólo el entorno de host. Una razón principal para este cambio es la introducción en ES6 de las Promesas, que discutiremos en el Capítulo 3, porque requieren la habilidad de tener un control directo y detallado sobre las operaciones de programación en la cola del bucle de eventos (ver la discusión de setTimeout(.. 0) en la sección «Cooperación»).

Ejecución paralela


Es muy común mezclar los términos «asíncrono» y «paralelo», pero en realidad son bastante diferentes. Recuerda, asíncrono se refiere a la brecha entre ahora y después. Pero paralelo se trata de que las cosas puedan ocurrir simultáneamente.

Las herramientas más comunes para la computación paralela son los procesos y subprocesos(hilos). Procesos y subprocesos se ejecutan de forma independiente y pueden ejecutarse simultáneamente: en procesadores separados, o incluso en ordenadores separados, pero múltiples subprocesos pueden compartir la memoria de un solo proceso.

Un bucle de eventos, por el contrario, rompe su trabajo en tareas y las ejecuta en serie, impidiendo el acceso paralelo y los cambios a la memoria compartida. El paralelismo y el «serialismo» pueden coexistir en forma de ciclos de eventos cooperativos en hilos separados.

El entrelazado de hilos de ejecución paralelos y el entrelazado de eventos asíncronos se producen en niveles muy diferentes de granulosidad.

Por ejemplo:

Mientras que todo el contenido de later() sería considerado como una sola entrada en la cola del bucle de eventos, al pensar en un hilo en el que se ejecutaría este código, en realidad hay quizás una docena de operaciones de bajo nivel diferentes. Por ejemplo, answer = answer * 2 requiere cargar primero el valor actual de la answer, luego poner 2 en alguna parte, luego realizar la multiplicación, luego tomar el resultado y almacenarlo de nuevo en answer.

En un entorno de un sólo hilo, no importa realmente que los elementos de la cola de hilos sean operaciones de bajo nivel, porque nada puede interrumpir el hilo. Pero si usted tiene un sistema paralelo, donde dos hilos diferentes están operando en el mismo programa, es muy probable que tenga un comportamiento impredecible.

Considerando lo siguiente:

En el comportamiento de hilo único de JavaScript, si foo() se ejecuta antes que bar(), el resultado es que a tiene 42, pero si bar() corre antes que foo() el resultado será 41.

Si los eventos de JS que comparten los mismos datos se ejecutan en paralelo, los problemas serían mucho más sutiles. Considere estas dos listas de tareas de pseudocódigo como los hilos que podrían respectivamente ejecutar el código en foo() y bar(), y considere qué sucede si se ejecutan exactamente al mismo tiempo:

Hilo 1 (X e Y son posiciones de memoria temporales):

Hilo 2 (X e Y son posiciones de memoria temporales):

Ahora, digamos que los dos hilos se están ejecutando realmente en paralelo. Probablemente puedas detectar el problema, ¿verdad? Utilizan las posiciones de memoria compartidas X e Y para sus pasos temporales.

¿Cuál es el resultado final si los pasos suceden así?

El resultado en a será 44. ¿Pero qué hay de este orden?

El resultado en a será 21.

Por lo tanto, la programación en hilos es muy delicada, porque si no se toman medidas especiales para evitar que este tipo de interrupción/entrelazado suceda, se puede obtener un comportamiento muy sorprendente, no determinista, que con frecuencia conduce a dolores de cabeza.

JavaScript nunca comparte datos a través de hilos, lo que significa que el nivel de indeterminismo no es una preocupación. Pero eso no significa que JS sea siempre determinista. ¿Recuerdas antes, donde el orden relativo de foo() y bar() produce dos resultados diferentes (41 o 42)?

Nota: Puede que no sea obvio todavía, pero no todo el indeterminismo es malo. A veces es irrelevante, y a veces es intencional. Veremos más ejemplos de ello a lo largo de este y los próximos capítulos.

Ejecución hasta la finalización

Debido al hilo único de JavaScript, el código dentro de foo() (y bar()) es atómico, lo que significa que una vez que foo() comienza a ejecutarse, la totalidad de su código terminará antes de que cualquiera de los códigos en bar() pueda hacerlo, o viceversa. Esto se denomina comportamiento de «ejecución-hasta-la-finalización».

De hecho, la semántica ejecución-hasta-la-finalización es más obvia cuando foo() y bar() tienen más código en ellos, como por ejemplo:

Debido a que foo() no puede ser interrumpido por bar(), y bar() no puede ser interrumpido por foo(), este programa sólo tiene dos resultados posibles dependiendo de cuál empiece a ejecutarse primero – si hilos múltiples fuesen posibles, y las sentencias individuales en foo() y bar() pudieran ser intercaladas, ¡el número de resultados posibles aumentaría enormemente!

El trozo 1 es síncrono (ocurre ahora), pero los trozos 2 y 3 son asíncronos (ocurren más tarde), lo que significa que su ejecución estará separada por un espacio de tiempo.

Trozo 1:

Trozo 2 (foo()):

Trozo 3 (bar()):

Las partes 2 y 3 pueden ocurrir en cualquier orden, por lo que hay dos posibles resultados para este programa, como se ilustra aquí:

Resultado 1:

Resultado 2:

Dos resultados del mismo código significa que todavía tenemos indeterminismo! Pero está en el nivel de orden de función (evento), más que a nivel de orden de sentencia (o, de hecho, en el nivel de orden de operación de expresión) como lo está con los hilos. En otras palabras, es más determinista de lo que habrían sido los hilos.

Tal como se aplica al comportamiento de JavaScript, este no determinismo que ordena funciones es el término común «condición de carrera», ya que foo() y bar() están compitiendo entre sí para ver cuál se ejecuta primero. Específicamente, es una «condición de carrera» porque no se puede predecir de manera fiable cómo resultarán a y b.

Nota: Si hubiera una función en JS que de alguna manera no tuviera un comportamiento de ejecución-hasta-la-finalización, podríamos tener muchos más resultados posibles, ¿verdad? Resulta que ES6 introduce tal cosa (ver Capítulo 4 «Generadores»), pero no se preocupe ahora mismo, ¡volveremos a eso!

Concurrencia


Imaginemos un sitio que muestra una lista de actualizaciones de estado (como un feed de noticias de una red social) que se carga progresivamente a medida que el usuario se desplaza por la lista. Para hacer que tal característica funcione correctamente, (al menos) dos «procesos» separados necesitarán ejecutarse simultáneamente (es decir, durante la misma ventana de tiempo, pero no necesariamente en el mismo instante).

Nota: Estamos usando «procesos» entre comillas porque no son verdaderos procesos a nivel de sistema operativo en el sentido de la informática. Son procesos virtuales, o tareas, que representan una serie de operaciones secuenciales conectadas lógicamente. Simplemente preferiremos «proceso» a «tarea» porque, desde el punto de vista terminológico, coincidirá con las definiciones de los conceptos que estamos explorando.

El primer «proceso» responderá a los eventos de desplazamiento (onscroll) (haciendo solicitudes Ajax de nuevo contenido) que se disparan cuando el usuario haya desplazado la página hacia abajo. El segundo «proceso» recibirá respuestas de Ajax (para renderizar el contenido en la página).

Obviamente, si un usuario se desplaza lo suficientemente rápido, puede ver que se disparan dos o más eventos de onscroll durante el tiempo que tarda en obtener la primera respuesta y procesarla, y por lo tanto va a tener eventos de onscroll y eventos de respuesta de Ajax disparándose rápidamente, entrelazándose entre sí.

La concurrencia es cuando dos o más «procesos» se ejecutan simultáneamente durante el mismo período, independientemente de si las operaciones de sus componentes individuales ocurren en paralelo (en el mismo instante en procesadores o núcleos separados) o no. Se puede pensar entonces en la concurrencia como paralelismo a nivel de «proceso» (o a nivel de tarea), en oposición al paralelismo a nivel de operación (hilos de procesador separados).

Nota: La concurrencia también introduce una noción opcional de estos «procesos» interactuando entre sí. Volveremos a eso más tarde.

Para una ventana de tiempo dada (unos segundos de desplazamiento de un usuario), visualicemos cada «proceso» independiente como una serie de eventos/operaciones:

«Proceso» 1 (eventos de desplazamiento):

«Proceso» 2 (eventos de respuesta de Ajax):

Es muy posible que un evento onscroll(de desplazamiento) y un evento de respuesta Ajax estén listos para ser procesados exactamente en el mismo momento. Por ejemplo, visualicemos estos eventos en una línea de tiempo:

Pero, volviendo a nuestra noción del bucle de eventos de antes en el capítulo, JS sólo va a ser capaz de manejar un evento a la vez, así que ya sea onscroll, la petición 2 va a suceder primero o la respuesta 1 va a suceder primero, pero no pueden suceder literalmente en el mismo momento. Al igual que los niños en la cafetería de una escuela, no importa la multitud que formen fuera de las puertas, ¡tendrán que formar una sola fila para conseguir su almuerzo!

Visualicemos el entrelazado de todos estos eventos en la cola del bucle de eventos.

Cola de bucle de eventos:

«El «Proceso 1» y el «Proceso 2» se ejecutan simultáneamente (en paralelo a nivel de tarea), pero sus eventos individuales se ejecutan secuencialmente en la cola del bucle de eventos.

Por cierto, ¿te has dado cuenta de cómo la respuesta 6 y la respuesta 5 se salieron del orden esperado?

El bucle de eventos de un solo hilo es una expresión de la concurrencia (ciertamente hay otros, a los que volveremos más adelante).

Sin interacción

Como dos o más «procesos» entrelazan sus pasos/eventos simultáneamente dentro del mismo programa, no necesitan forzosamente interactuar entre sí si las tareas no están relacionadas. Si no interactúan, el no determinismo es perfectamente aceptable.

Por ejemplo:

foo() y bar() son dos «procesos» simultáneos, y no se determina en qué orden serán lanzados. Pero hemos construido el programa de modo que no importa el orden en el que se lancen, porque actúan de forma independiente y como tales no necesitan interactuar.

Esto no es un error de «condición de carrera», ya que el código siempre funcionará correctamente, independientemente de la petición.

Interacción

Más comúnmente, los «procesos» concurrentes interactuarán necesariamente, indirectamente a través del alcance/ámbito y/o el DOM. Cuando tal interacción ocurra, usted tendrá que coordinar estas interacciones para prevenir «condiciones de carrera», como se describió anteriormente.

He aquí un ejemplo simple de dos «procesos» concurrentes que interactúan debido a un ordenamiento implícito, que sólo a veces se rompe:

Los «procesos» concurrentes son las dos llamadas response() que se harán para manejar las respuestas Ajax. Pueden ocurrir en cualquier orden.

Supongamos que el comportamiento esperado es que res[0] tiene los resultados de la llamada "http://some.url.1", y res[1] tiene los resultados de la llamada "http://some.url.2". Algunas veces ese será el caso, pero otras veces se invertirán, dependiendo de cuál de las llamadas termine primero. Hay una muy buena probabilidad de que este indeterminismo sea un error de «condición de carrera».

Nota: Sea extremadamente cauteloso con las suposiciones que usted podría tender a hacer en estas situaciones. Por ejemplo, no es raro que un desarrollador observe que "http://some.url.2" es «siempre» mucho más lento en responder que "http://some.url.1", tal vez en virtud de las tareas que está realizando (por ejemplo, realizando una tarea de base de datos y la otra simplemente obteniendo un archivo estático), por lo que el orden observado parece ser siempre el esperado. Incluso si ambas peticiones van al mismo servidor y éste responde intencionadamente en un orden determinado, no hay garantía real de qué orden volverán a llegar las respuestas al navegador.

Por lo tanto, para hacer frente a esta condición de carrera, puede coordinar la interacción de peticiones:

Independientemente de cuál sea la respuesta de Ajax que regrese primero, inspeccionamos data.url(¡suponiendo que sea devuelto desde el servidor, por supuesto!) para averiguar qué posición deben ocupar los datos de respuesta en el array de res. res[0] siempre contendrá los resultados de "http://some.url.1" y res[1] siempre contendrá los resultados de "http://some.url.2". A través de una simple coordinación, eliminamos el indeterminismo de la «condición de carrera».

El mismo razonamiento de este escenario se aplicaría si múltiples llamadas de función concurrentes estuvieran interactuando entre sí a través del DOM compartido, como una actualizando el contenido de un <div> y la otra actualizando el estilo o atributos del <div> (por ejemplo, para que el elemento DOM sea visible una vez que tenga contenido). Probablemente no querrá mostrar el elemento DOM antes de que tenga contenido, por lo que la coordinación debe asegurar una correcta interacción en el orden.

Algunos escenarios de concurrencia siempre se rompen (no sólo algunas veces) sin una interacción coordinada. Considere:

En este ejemplo, independiente de cual de los dos, foo() o bar(), se lanze primero, siempre causará que baz() se ejecute demasiado pronto (a o b será indefinido), pero la segunda invocación de baz() funcionará, ya que a y b estarán disponibles.

Hay diferentes maneras de tratar esta condición. Aquí hay una manera simple:

El si (a && b) condicional alrededor de la llamada baz() es tradicionalmente llamado una «puerta», porque no estamos seguros de en qué orden llegarán a y b, pero esperamos a que ambos lleguen allí antes de proceder a abrir la puerta (llamada baz()).

Otra condición de interacción de concurrencia con la que puede encontrarse es a veces llamada «carrera», pero más correctamente llamada «cerrojo». Se caracteriza por un comportamiento de «sólo el primero gana». Aquí, el no determinismo es aceptable, en el sentido de que estás diciendo explícitamente que está bien que la «carrera» hasta la línea de meta tenga un solo ganador.

Considere este código erróneo:

Cualquiera que sea el último que se lance (foo() o bar()) no sólo sobrescribirá el valor asignado del otro, sino que también duplicará la llamada a baz() (probablemente no deseada).

Por lo tanto, podemos coordinar la interacción con un simple «cerrojo», para dejar pasar sólo el primero:

La condición if(a == indefinido) sólo permite que pase el primero de foo() o bar(), y la segunda (y de hecho cualquier llamada subsiguiente) simplemente sería ignorada. No hay honor en quedar en segundo lugar!

Nota: En todos estos escenarios, hemos estado usando variables globales con fines de ilustración simplista, pero no hay nada en nuestro razonamiento que lo requiera. Siempre y cuando las funciones en cuestión puedan acceder a las variables (vía ámbito), funcionarán según lo previsto. Confiar en variables de alcance léxico (ver el título Scope & Closures de esta serie de libros), y de hecho en variables globales como en estos ejemplos, es una desventaja obvia para estas formas de coordinación de la concurrencia. A medida que avancemos en los próximos capítulos, veremos otras formas de coordinación que son mucho más limpias en ese sentido.

Cooperación

Otra expresión de la coordinación de la concurrencia se llama «concurrencia cooperativa». Aquí, el foco no está tanto en interactuar a través de compartir el valor en los alcances (¡aunque obviamente eso todavía está permitido!). El objetivo es tomar un «proceso» a largo plazo y dividirlo en pasos o lotes para que otros «procesos» concurrentes tengan la oportunidad de intercalar sus operaciones en la cola del bucle de eventos.

Por ejemplo, considere un manejador de respuesta Ajax que necesite ejecutar una larga lista de resultados para transformar los valores. Usaremos Array#map(..) para mantener el código más corto:

Si "http://some.url.1" obtiene sus resultados primero, la lista completa será mapeada en res de una sola vez. Si se trata de unos pocos miles de registros o menos, esto no suele ser gran cosa. Pero si se trata de 10 millones de registros, eso puede tardar un poco en ejecutarse (varios segundos en un portátil potente, mucho más tiempo en un dispositivo móvil, etc.).

Mientras se está ejecutando un «proceso» de este tipo, no puede ocurrir nada más en la página, incluyendo ninguna otra llamada response(..), ninguna actualización de la interfaz de usuario, ni siquiera eventos de usuario como desplazarse, escribir, hacer clic en botones, etc. Eso es bastante doloroso.

Por lo tanto, para hacer un sistema más cooperativo y concurrente, más amigable y que no acapare la cola del bucle de eventos, puede procesar estos resultados en lotes asíncronos, y después cada uno «volviendo» de nuevo al bucle de eventos para permitir que otros eventos de espera ocurran.

He aquí un enfoque muy simple:

Procesamos el conjunto de datos en trozos de un máximo de 1.000 elementos. De este modo, aseguramos un «proceso» a corto plazo, aunque eso signifique muchos más «procesos» posteriores, ya que el entrelazado en la cola del bucle de eventos nos dará un sitio/app mucho más responsivo.

Por supuesto, no estamos coordinando interactivamente el orden de ninguno de estos «procesos», por lo que el orden de los resultados en res no será predecible. Si se quisiese ordenar, necesitaría usar técnicas de interacción como las que discutimos anteriormente, o las que cubriremos en capítulos posteriores de este libro.

Usamos el setTimeout(..0) (truco) para la programación de sincronización, lo que básicamente significa «pegar esta función al final de la cola de eventos actual».

Nota: setTimeout(..0) no está técnicamente insertando un elemento directamente en la cola de bucle de eventos. El temporizador insertará el evento en su próxima oportunidad. Por ejemplo, dos llamadas posteriores a setTimeout(..0) no estarían estrictamente garantizadas para ser procesadas en orden de llamada, por lo que es posible ver varias condiciones como la deriva del temporizador donde el orden de tales eventos no es predecible. En Node.js, un enfoque similar es process.nextTick(..). A pesar de lo conveniente (y normalmente más eficiente) que sería, no hay una sola forma directa (al menos todavía) a través de todos los entornos para asegurar el orden de los eventos de sincronización. Abordaremos este tema con más detalle en la siguiente sección.

Trabajos


A partir de ES6, hay un nuevo concepto encima de la cola de eventos, llamado «cola de trabajos». El uso más probable es con el comportamiento asincrónico de las Promesas de ES6 (ver Capítulo 3).

Desafortunadamente, por el momento es un mecanismo sin una API expuesta, y por lo tanto demostrarlo es un poco más complicado. Así que vamos a tener que describirlo conceptualmente, de manera que cuando discutamos el comportamiento asíncrono con Promesas en el Capítulo 3, entenderás cómo se están programando y procesando esas acciones.

Por lo tanto, la mejor manera de pensar sobre esto que he encontrado es que la «cola de trabajos» es una cola que cuelga al final de cada «tick» en la cola del bucle de eventos. Ciertas acciones implícitamente asíncronas que pueden ocurrir durante un «tick» del bucle de eventos no causarán que se añada un evento completamente nuevo a la cola de bucles de eventos, sino que añadirán un elemento (también conocido como Trabajo) al final de la cola de trabajos del «tick» actual.

Es como decir, «oh, aquí está esta otra cosa que necesito hacer más tarde, pero asegúrate de que suceda de inmediato antes de que algo más pueda suceder».

O, para usar una metáfora: la cola del bucle de eventos es como un paseo en un parque de atracciones, donde una vez que terminas el paseo, tienes que ir al final de la fila para montar de nuevo. Pero la cola de trabajos es como terminar el viaje, pero luego hacer cola y volver a empezar.

Un trabajo también puede causar que se añadan más trabajos al final de la misma cola. Por lo tanto, es teóricamente posible que un «bucle» de Trabajo (un Trabajo que sigue añadiendo otro Trabajo, etc.) pueda girar indefinidamente, privando así al programa de la capacidad de pasar al siguiente evento del bucle. Esto sería conceptualmente casi lo mismo que expresar un bucle largo o infinito (como while(true)...) en tu código.

Los trabajos son algo así como el espíritu del truco setTimeout(..0), pero implementados de tal manera que tengan un orden mucho más definido y garantizado: más tarde, pero tan pronto como sea posible.

Imaginemos una API para programar trabajos (directamente, sin trucos), y llamémosla schedule(...). Considere:

Puede esperar que esto imprima A B C D, pero en su lugar imprimirá A C D B, porque los Trabajos se ejecutan al final del bucle del evento actual y el temporizador se dispara para programar el siguiente bucle del evento (¡si está disponible!).

En el Capítulo 3, veremos que el comportamiento asincrónico de las Promesas se basa en Trabajos, por lo que es importante tener claro cómo se relaciona con el comportamiento del bucle de eventos.

Orden de estados


El orden en el que expresamos las declaraciones en nuestro código no es necesariamente el mismo orden en el que el motor JS las ejecutará. Puede parecer una afirmación bastante extraña, así que la exploraremos brevemente.

Pero antes de hacerlo, debemos ser muy claros en algo: las reglas/gramática del lenguaje (ver el título de Tipos y Gramática de esta serie de libros) dictan un comportamiento muy predecible y confiable para ordenar las declaraciones desde el punto de vista del programa. Así que lo que estamos a punto de discutir no son cosas que deberías ser capaz de observar en tu programa JS.

Advertencia: Si alguna vez puede observar cómo se reordenan las sentencias del compilador como estamos a punto de ilustrar, eso sería una clara violación de la especificación, e incuestionablemente sería debido a un error en el motor JS en cuestión — ¡uno que debería ser reportado y corregido de inmediato! Pero es mucho más común que sospeches que algo loco está ocurriendo en el motor JS, cuando en realidad es sólo un error (¡probablemente una «condición de carrera»!) en tu propio código — así que mira allí primero, y una y otra vez y otra vez. El depurador JS, usando puntos de ruptura y pasando a través de código línea por línea, será su herramienta más poderosa para olfatear tales errores en su código.

Considere:

Este código no tiene ninguna asincronía expresada en él (con excepción del rara salida/entrada asíncrona de console discutida anteriormente), así que la suposición más probable es que procesaría línea por línea de manera descendente.

Pero es posible que el motor JS, después de compilar este código (sí, JS está compilado — ¡véase el título Scope & Closures de esta serie de libros!) pueda encontrar oportunidades para ejecutar su código más rápido reorganizando (de forma segura) el orden de estas sentencias. Esencialmente, mientras no puedas observar la reordenación, cualquier cosa es justa.

Por ejemplo, el motor puede encontrar que es más rápido ejecutar el código de esta manera:

O esta:

O incluso:

En todos estos casos, el motor JS está realizando optimizaciones seguras durante su compilación, ya que el resultado final observable será el mismo.

Pero aquí hay un escenario en el que estas optimizaciones específicas serían inseguras y por lo tanto no se podrían permitir (por supuesto, por no decir que no están optimizadas en absoluto):

Otros ejemplos en los que la reordenación del compilador podría crear efectos secundarios observables (y por lo tanto deben ser rechazados) incluirían cosas como cualquier llamada a una función con efectos secundarios (incluso y especialmente funciones getter), u objetos Proxy ES6 (ver el título ES6 & Beyond de esta serie de libros).

Considere:

Si no fuera por las sentencias console.log(..) en este fragmento (sólo usadas como una forma conveniente de efecto secundario observable para la ilustración), el motor JS probablemente habría estado libre, si quisiera (¿quién sabe si lo estaría?), para reordenar el código:

Mientras que la semántica de JS afortunadamente nos protege de las pesadillas observables de las que la reordenación de sentencias del compilador parecería estar en peligro, todavía es importante entender cuán tenue es el vínculo entre la forma en que se crea el código fuente (de arriba hacia abajo) y la forma en que se ejecuta después de la compilación.

La reordenación de sentencias del compilador es casi una micro-metafora para la concurrencia y la interacción. Como concepto general, este conocimiento puede ayudarle a entender mejor los problemas de flujo de código JS asíncrono.

Revisión


Un programa JavaScript está (prácticamente) siempre dividido en dos o más partes, donde la primera parte se ejecuta ahora y la siguiente parte se ejecuta más tarde, en respuesta a un evento. A pesar de que el programa se ejecuta trozo-a-trozo, todos ellos comparten el mismo acceso al alcance y estado del programa, por lo que cada modificación del estado se hace encima del estado anterior.

Siempre que hay eventos que ejecutar, el bucle de eventos se ejecuta hasta que la cola está vacía. Cada iteración del bucle de eventos es un «tick». La interacción del usuario, la entrada/salida, y los temporizadores encolan eventos en la cola de eventos.

En un momento dado, sólo se puede procesar un evento de la cola a la vez. Mientras se ejecuta un evento, puede causar directa o indirectamente uno o más eventos posteriores.

La concurrencia es cuando dos o más cadenas de eventos se entrelazan a lo largo del tiempo, de tal forma que desde una perspectiva de alto nivel, parecen estar ejecutándose simultáneamente (aunque en un momento dado sólo se esté procesando un evento).

A menudo es necesario hacer alguna forma de coordinación de interacción entre estos «procesos» concurrentes (a diferencia de los procesos del sistema operativo), por ejemplo para asegurar el orden o para prevenir «condiciones de carrera». Estos «procesos» también pueden cooperar rompiéndose en trozos más pequeños y permitiendo que otros «procesos» se entrelazen.


Capítulo 2: Callbacks


En el Capítulo 1, exploramos la terminología y los conceptos en torno a la programación asíncrona en JavaScript. Nos centramos en comprender la cola de eventos de un solo hilo (uno a la vez) que dirige todos los «eventos» (invocaciones de funciones de sincronización). También exploramos varias maneras en las que los patrones de concurrencia explican las relaciones (¡si las hay!) entre cadenas de eventos o «procesos» que se ejecutan simultáneamente (tareas, llamadas de función, etc.).

Todos nuestros ejemplos en el Capítulo 1 usaron la función como la unidad individual e indivisible de operaciones, donde dentro de la función, las sentencias se ejecutan en orden predecible (¡por encima del nivel del compilador!), pero en el nivel de orden de función, los eventos (también conocidos como invocaciones de función asíncronas) pueden ocurrir en una variedad de órdenes.

En todos estos casos, la función actúa como una «llamada de retorno» (o callback), ya que sirve como destino para que el bucle de eventos «llame de nuevo» al programa, siempre que se procese ese elemento de la cola.

Como sin duda habrán observado, los callbacks son, con mucho, la forma más común de expresar y gestionar la asincronía en los programas de JS. De hecho, el callback es el patrón más fundamental de asincronía en el lenguaje.

Innumerables programas de JS, incluso los muy sofisticados y complejos, no han sido escritos sobre ninguna otra base asíncrona que el callback (con por supuesto los patrones de interacción de concurrencia que exploramos en el Capítulo 1). La función callback es el caballo de trabajo asíncrono para JavaScript, y hace su trabajo de manera respetable.

Excepto… que los callbacks no están exentas de sus defectos. Muchos desarrolladores están entusiasmados con las promesas (¡intentando hacer un juego de palabras!) de mejores patrones de sincronización. Pero es imposible usar cualquier abstracción de manera efectiva si no se entiende qué es lo que está abstrayendo y por qué.

En este capítulo, exploraremos un par de ellas en profundidad, como motivación de por qué son necesarios y deseados patrones de sincronización más sofisticados (explorados en los capítulos siguientes de este libro).

Continuaciones


Volvamos al ejemplo de callbacks de asíncronos con el que empezamos en el Capítulo 1, pero permítanme que lo modifique ligeramente para ilustrar un punto:

A y B representan la primera mitad del programa (es decir, el ahora), y C marca la segunda mitad del programa (es decir, el después). La primera mitad se ejecuta inmediatamente, y luego hay una «pausa» de duración indeterminada. En algún momento futuro, si la llamada de Ajax se completa, entonces el programa continuará donde lo dejó, y continuará con la segunda mitad.

En otras palabras, la función callback envuelve o encapsula la continuación del programa.

Hagamos el código aún más simple:

Deténgase un momento y pregúntese cómo describiría (a otra persona menos informada sobre el funcionamiento de JS) el comportamiento de ese programa. Adelante, inténtalo en voz alta. Es un buen ejercicio que ayudará a que mis próximos puntos tengan más sentido.

La mayoría de los lectores ahora mismo probablemente pensaron o dijeron algo parecido a: «Haz A, luego establece un tiempo de espera de 1.000 milisegundos, y una vez que se dispare, haz C.» ¿Qué tan cerca estuvo tu planteamiento?

Podrías haberte atrapado a ti mismo y haberte auto-editado: «Haz A, configura el tiempo de espera para 1.000 milisegundos, luego haz B, luego después de que el tiempo de espera pase, haz C.» Eso es más exacto que la primera versión. ¿Puedes notar la diferencia?

Aunque la segunda versión es más precisa, ambas versiones son deficientes a la hora de explicar este código de manera que nuestro cerebro coincida con el código, y el código con el motor JS. La desconexión es a la vez sutil y monumental, y está en el corazón mismo de la comprensión de las deficiencias de los callbacks como expresión y gestión de sincronización.

Tan pronto como introducimos una sola continuación (¡o varias docenas como lo hacen muchos programas!) en la forma de una función callback, hemos permitido que se forme una divergencia entre cómo funciona nuestro cerebro y la forma en que operará el código. Cada vez que estos dos divergen (y este no es por mucho el único lugar que ocurre, como estoy seguro que sabes!), nos encontramos con el hecho inevitable de que nuestro código se vuelve más difícil de entender, razonar, depurar y mantener.

Cerebro Secuencial


Estoy bastante seguro de que la mayoría de los lectores han oído a alguien decir (incluso usted mismo lo ha dicho), «Soy multitarea». Los efectos de tratar de actuar como multitarea van desde lo humorístico (por ejemplo, el tonto juego de dar palmaditas en la cabeza con el estómago) hasta lo mundano (masticar chicle mientras se camina) y lo peligroso (enviar mensajes de texto mientras se conduce).

¿Pero somos multitareas? ¿Podemos realmente hacer dos acciones conscientes e intencionales a la vez y pensar/razonar sobre ambas en exactamente el mismo momento? ¿Nuestro más alto nivel de funcionalidad cerebral tiene multi hilos paralelos en marcha?

La respuesta puede sorprenderle: probablemente no.

Así no es como nuestros cerebros parecen estar preparados. Somos mucho más sencillos de lo que muchos de nosotros (¡especialmente personalidades de tipo A!) nos gustaría admitir. Sólo podemos pensar en una cosa en un instante dado.

No estoy hablando de todas nuestras funciones cerebrales involuntarias, subconscientes y automáticas, como los latidos del corazón, la respiración y el parpadeo. Todas estas son tareas vitales para mentenernos vivos, pero no son cerebalmente intencionales. Afortunadamente, mientras nos obsesionamos con revisar las redes sociales por decimoquinta vez en tres minutos, nuestro cerebro sigue en segundo plano (¡hilos!) con todas esas tareas importantes.

En lugar de eso, estamos hablando de cualquier tarea que esté en primera línea de nuestras mentes en este momento. Para mí, es escribir el texto de este libro ahora mismo. ¿Estoy haciendo alguna otra función cerebral de nivel superior exactamente en este mismo momento? No, en realidad no. Me distraigo rápida y fácilmente — ¡unas cuantas docenas de veces en estos últimos párrafos!

Cuando falsificamos la multitarea, como tratar de escribir algo al mismo tiempo que hablamos por teléfono con un amigo o familiar, lo más probable es que lo que estemos haciendo sea actuar como conmutadores rápidos de contexto. En otras palabras, cambiamos de un lado a otro entre dos o más tareas en rápida sucesión, progresando simultáneamente en cada tarea en pequeños y rápidos trozos. Lo hacemos tan rápido que al mundo exterior parece como si estuviéramos haciendo estas cosas en paralelo.

¿Suena eso sospechosamente como una concurrencia evasiva de asíncronía (como la que ocurre en JS)? Si no, ¡regresa y lee el Capítulo 1 otra vez!

De hecho, una forma de simplificar (es decir, abusar) del mundo masivamente complejo de la neurología en algo que puedo esperar discutir aquí remotamente es que nuestros cerebros funcionan como una especie de cola de bucle de eventos.

Si piensas en cada letra (o palabra) que escribo como un único evento de sincronización, sólo en esta frase hay varias docenas de oportunidades para que mi cerebro sea interrumpido por algún otro evento, como por mis sentidos, o incluso mis pensamientos aleatorios.

No me interrumpen y me arrastran a otro «proceso» cada vez que puedo (¡por suerte, o este libro nunca se escribiría!). Pero sucede lo suficientemente a menudo como para sentir que mi propio cerebro está cambiando casi constantemente a varios contextos diferentes (también conocidos como «procesos»). Y eso es muy parecido a como se sentiría el motor JS.

Haciendo Versus Planeando

OK, así que nuestros cerebros pueden ser pensados como una operación en una cola de eventos de un solo hilo, como lo hace el motor JS. Eso suena más realista.

Pero tenemos que matizar más que eso en nuestro análisis. Hay una gran diferencia observable entre la forma en que planificamos las distintas tareas y la forma en que nuestros cerebros las llevan a cabo.

De nuevo, volvamos a la escritura de este texto como mi metáfora. Mi plan mental general aquí es seguir escribiendo y escribiendo, yendo secuencialmente a través de una serie de puntos que he ordenado en mis pensamientos. No planeo tener ninguna interrupción o actividad no lineal en este escrito. Pero aún así, mi cerebro está cambiando todo el tiempo.

A pesar de que a nivel operativo nuestros cerebros están sincronizados, parece que planificamos las tareas de forma secuencial y sincrónica. «Necesito ir a la tienda, comprar un poco de leche, y luego dejar mi ropa en la tintorería.»

Notarás que este pensamiento (planificación) de nivel superior no parece muy asíncrono en su formulación. De hecho, es raro que pensemos deliberadamente sólo en términos de eventos. En lugar de eso, planificamos las cosas cuidadosamente, secuencialmente (A luego B luego C), y asumimos hasta cierto punto una especie de bloqueo temporal que obliga a B a esperar a A, y a C a esperar a B.

Cuando un desarrollador escribe código, está planeando una serie de acciones para que ocurran. Si se les da bien ser desarrolladores, lo planean cuidadosamente. «Necesito poner z al valor de x, y luego x al valor de y,» y así sucesivamente.

Cuando escribimos código síncrono, declaración por declaración, funciona de manera muy parecida a nuestra lista de cosas por hacer:

Estas tres sentencias de asignación son síncronas, así que x = y espera a que z = x termine, e y = z a su vez espera a que x = y termine. Otra forma de decirlo es que estas tres afirmaciones están temporalmente obligadas a ejecutarse en un orden determinado, una tras otra. Afortunadamente, no necesitamos que nos molesten con ningún detalle asíncrono aquí. Si lo hicimos, el código se vuelve mucho más complejo, ¡rápidamente!

De modo que si la planificación cerebral síncrona mapea bien a las declaraciones de código síncronas, ¿qué tan bien lo hacen nuestros cerebros en la planificación de código asíncrono?

Resulta que la forma en que expresamos la asincronía (con callbacks) en nuestro código no se relaciona muy bien con esa conducta de planificación cerebral síncrona.

¿Te imaginas tener una línea de pensamiento que planifique tus mandados así?

«Tengo que ir a la tienda, pero de camino estoy seguro de que me llamarán por teléfono, así que ‘Hola, mamá’, y mientras ella empieza a hablar, voy a buscar la dirección de la tienda en el GPS, pero eso tomará un segundo para cargar, así que voy a bajar el volumen de la radio para que pueda escuchar mejor a mamá, entonces me daré cuenta de que olvidé ponerme una chaqueta y hace frío afuera, pero no importa, sigue conduciendo y hablando con mamá, y luego el sonido del cinturón de seguridad me recuerda que me abroche el cinturón de seguridad, así que ‘Sí, mamá, llevo puesto el cinturón de seguridad, ¡siempre lo hago!’. Ah, finalmente el GPS consiguió las direcciones, ahora…»

Suena rídículo este planteamiento de cómo planificamos nuestro día y pensamos en qué hacer y en qué orden, sin embargo, es exactamente cómo nuestros cerebros operan a un nivel funcional. Recuerda, eso no es multitarea, es sólo un rápido cambio de contexto.

La razón por la que es difícil para nosotros como desarrolladores escribir código de eventos asíncrono, especialmente cuando todo lo que tenemos es el callback para hacerlo, es que el flujo conciente pensar/planificar es antinatural para la mayoría de nosotros.

Pensamos en términos paso a paso, pero las herramientas (callbacks) disponibles para nosotros en código no se expresan paso a paso una vez que pasamos de síncrono a asíncrono.

Y es por eso que es tan difícil escribir con precisión y razonar sobre la sincronía de código JS con los callbacks: porque no es así como funciona nuestra planificación cerebral.

Nota: ¡La única cosa peor que no saber por qué algunos códigos se rompen es no saber por qué funcionó en primer lugar! Es la clásica mentalidad de «casa de naipes»: «funciona, pero no estoy seguro de por qué, así que nadie lo toca!» Tal vez hayas oído: «El infierno es otra gente» (Sartre), y la versión del programador, «El infierno es el código de otra gente». Yo creo sinceramente: «El infierno es no entender mi propio código.»Y las los callbacks son una de las principales culpables.

Callbacks anidados/encadenados

Considere:

Este es un código oportuno que es reconocible para tí. Tenemos una cadena de tres funciones anidadas juntas, cada una representando un paso en una serie asíncrona (tarea, «proceso»).

Este tipo de código es a menudo llamado «callback hell» (infierno de devolución de llamada), y a veces también se le llama «pirámide de perdición» (por su forma triangular orientada hacia los lados debido a la sangría anidada).

Pero «callback hell» en realidad no tiene casi nada que ver con el anidamiento/indentación. Es un problema mucho más profundo que eso. Veremos cómo y por qué mientras continuamos con el resto de este capítulo.

Primero, estamos esperando el evento «clic», luego estamos esperando a que se dispare el temporizador y luego estamos esperando a que vuelva la respuesta del Ajax, en cuyo momento podría volver a hacerlo todo de nuevo.

A primera vista, este código puede parecer mapear su asincronía naturalmente a la planificación cerebral secuencial.

Primero (ahora), nosotros:

Entonces luego, nosotros:

Y aún más tarde, nosotros:

Y finalmente (más tarde), nosotros:

Pero hay varios problemas con el razonamiento sobre este código linealmente de esta manera.

Primero, es un casualidad del ejemplo que nuestros pasos estén en líneas subsiguientes (1, 2, 3, y 4…). En los programas JS asíncronos reales, a menudo hay mucho más ruido entorpeciendo las cosas, ruido que tenemos que pasar hábilmente en nuestro cerebro mientras saltamos de una función a la siguiente. Entender el flujo asíncrono en este código cargado de callbacks no es imposible, pero ciertamente no es natural o fácil, incluso con mucha práctica.

Pero también, hay algo más profundamente incorrecto, que no es evidente sólo en ese ejemplo de código. Permítanme crear otro escenario (pseudocódigo) para ilustrarlo:

Mientras que los experimentados entre ustedes identificarán correctamente el verdadero orden de las operaciones aquí, apuesto a que es más que un poco confuso a primera vista, y toma algunos ciclos mentales concertados para llegar. Las operaciones se realizarán en este orden:

  • doA()
  • doF()
  • doB()
  • doC()
  • doE()
  • doD()

¿Lo hiciste bien la primera vez que miraste el código?

Vale, algunos de vosotros estáis pensando que fui injusto en mi nombramiento de funciones, al llevaros intencionadamente por el mal camino. Juro que sólo estaba nombrando en orden de apariencia de arriba hacia abajo. Pero déjame intentarlo de nuevo:

Ahora, los he nombrado alfabéticamente en orden de ejecución real. Pero todavía apuesto, incluso con la experiencia ahora en este escenario, trazando a través del orden A -> B -> C -> C -> D -> E -> F que no se hace natural a muchos de ustedes lectores. Ciertamente, tus ojos hacen un montón de saltos arriba y abajo en el fragmento de código, ¿verdad?

Pero incluso si todo eso te resulta natural, todavía hay un peligro más que podría causar estragos. ¿Puedes ver lo que es?

¿Y si doA(…) o doD(…) no son realmente asíncronos, de la forma en que obviamente los asumimos? Uh oh, ahora el orden es diferente. Si ambos son síncronos (y tal vez sólo algunas veces, dependiendo de las condiciones del programa en ese momento), el orden es ahora A -> C -> D -> F -> E -> B.

Ese sonido que acabas de escuchar débilmente en el fondo son los suspiros de miles de desarrolladores de JS que acaban de levarse las manos a la cara.

¿Es la anidación el problema? ¿Es eso lo que hace tan difícil rastrear el flujo de sincronización? Eso es parte de ello, ciertamente.

Pero permítanme reescribir el ejemplo anterior anidado de evento/timeout/Ajax sin usar anidamiento:

Este planteamienton del código no tiene los problemas de anidamiento/indentación de su forma previa, y sin embargo es tan susceptible al «infierno de el callback» como antes. Por qué?

A medida que pasamos a razonar linealmente (secuencialmente) sobre este código, tenemos que saltar de una función, a la siguiente, a la siguiente, y rebotar alrededor de la base de código para «ver» el flujo de la secuencia. Y recuerda, esto es código simplificado en el mejor de los casos. Todos sabemos que las bases de código del programa JS asíncrono real son a menudo fantásticamente más desordenadas, lo que hace que tales órdenes de razonamiento de magnitud sean más difíciles.

Otra cosa a notar: para enlazar los pasos 2, 3, y 4 para que sucedan en sucesión, los únicos callbacks por sí solas nos llevan a hardacodear (establecer manualmente) el paso 2 en el paso 1, el paso 3 en el paso 2, paso 4 en el paso 3, y así sucesivamente. El hardcodeado no es necesariamente algo malo, si realmente es una condición fija que el paso 2 siempre debe llevar al paso 3.

Pero el hardcodeado definitivamente hace que el código sea un poco más quebradizo, ya que no tiene en cuenta nada que vaya mal que pueda causar una desviación en la progresión de los pasos. Por ejemplo, si el paso 2 falla, el paso 3 nunca se alcanza, ni el paso 2 se reintenta, o se mueve a un flujo de manejo de errores alternativo, y así sucesivamente.

Todos estos problemas son cosas que puedes hardcodear manualmente en cada paso, pero ese código es a menudo muy repetitivo y no es reutilizable en otros pasos o en otros flujos de asincronía en tu programa.

Aunque nuestros cerebros puedan planear una serie de tareas de manera secuencial (esto, entonces esto, entonces esto,…), la naturaleza de eventos de nuestra operación cerebral hace que la recuperación/intento/bifurcación del control de flujo sea casi sin esfuerzo. Si está haciendo recados y se da cuenta de que dejó la lista de compras en casa, no termina el día porque no lo planeó de antemano. Su cerebro resuelve de este contratiempo fácilmente: usted va a casa, toma la lista, y luego regresa directamente a la tienda.

Pero la naturaleza frágil de los callbacks hardcodeados (incluso con el manejo de errores hardcodeados) es a menudo mucho menos elegante. Una vez que usted termina especificando (es decir, pre veiendo) todas las varias eventualidades/rutas, el código se vuelve tan complicado que es difícil mantenerlo o actualizarlo.

De eso se trata el «infierno de los callbacks»! El anidamiento/indentación es básicamente un espectáculo lateral, una pista falsa.

Y como si todo eso no fuera suficiente, ni siquiera hemos tocado lo que sucede cuando dos o más cadenas de estas llamadas de retorno están sucediendo simultáneamente, o cuando el tercer paso se bifurca en llamadas de retorno «paralelas» con puertas o cerrojos, o… Dios mío, me duele el cerebro, ¿y el tuyo?

¿Estás captando la noción aquí de que nuestras conductas de planificación cerebral secuencial y bloqueante no se mapean bien con el código asíncrono orientado a callbacs? Esa es la primera deficiencia importante que hay que articular acerca de los callbacks: expresan la asincronía en código de manera que nuestros cerebros tienen que luchar sólo para mantenerse en sincronía (¡juego de palabras intencionado!).

Problemas de confianza


El desajuste entre la planificación cerebral secuencial y y el código asíncrono por miedo de callbacks de JS es sólo una parte del problema con los callbacks. Hay algo mucho más profundo por lo que preocuparse.

Revisemos una vez más la noción de una función callback como la continuación (también conocida como la segunda mitad) de nuestro programa:

// A y // B suceden ahora, bajo el control directo del programa principal de JS. Pero // C se aplaza para que suceda más tarde, y bajo el control de otra parte — en este caso, la función ajax(..). En un sentido básico, ese tipo de transferencia de control no suele causar muchos problemas a los programas.

Pero piense que por su infrecuencia que este conmutador de control no es un gran problema. De hecho, es uno de los peores (y sin embargo más sutiles) problemas acerca del diseño impulsado por callbacks. Gira en torno a la idea de que a veces ajax(...) (es decir, la «parte» a la que le das la continuación del callback) no es una función que tú escribiste, o que tú controlas directamente. Muchas veces, es una utilidad proporcionada por un tercero.

A esto lo llamamos «inversión de control», cuando usted participa en su programa y cede el control de su ejecución a terceros. Existe un «contrato» tácito entre su código y la utilidad de terceros, un conjunto de cosas que espera que se mantengan.

Cuento de cinco callbacks

Puede que no sea tan obvio por qué esto es tan importante. Permítanme construir un escenario exagerado para ilustrar los peligros de la confianza en juego.

Imagine que usted es un desarrollador encargado de construir un sistema de pago de comercio electrónico para un sitio que vende televisores caros. Ya tiene todas las páginas del sistema de pago bien construidas. En la última página, cuando el usuario hace clic en «confirmar» para comprar el televisor, es necesario llamar a una función de terceros (proporcionada, por ejemplo, por alguna compañía de seguimiento analítico) para que la venta pueda ser rastreada.

Usted nota que han proporcionado lo que parece una utilidad de seguimiento de sincronización, probablemente por el bien de las mejores prácticas de rendimiento, lo que significa que necesita pasar una función callback. En esta continuación que usted pasa, usted tendrá el código final que carga la tarjeta de crédito del cliente y muestra la página de agradecimiento.

Este código podría parecerse a:

Bastante fácil, ¿verdad? Usted escribe el código, lo prueba, todo funciona y se implementa en producción. ¡Todo el mundo está feliz!

Pasaron seis meses y no hubo problemas. Casi olvidas que hasta escribiste ese código. Una mañana, usted está en una cafetería antes del trabajo, disfrutando despreocupadamente de su café con leche, cuando recibe una llamada de pánico de su jefe insistiendo en que deje el café y se apresure a ir a trabajar de inmediato.

Cuando llegas, descubres que a un cliente de alto perfil se le ha cargado cinco veces a su tarjeta de crédito por el mismo televisor, y es comprensible que esté molesto. El servicio de atención al cliente ya ha emitido una disculpa y procesado un reembolso. Pero tu jefe quiere saber cómo ha podido pasar esto. «¿No tenemos pruebas para estas cosas?»

Ni siquiera recuerdas el código que escribiste. Pero vuelves a cavar y empiezas a tratar de averiguar qué pudo haber salido mal.

Después de escarbar en algunos registros, se llega a la conclusión de que la única explicación es que la utilidad de análisis de alguna manera, por alguna razón, llamó a su devolución de llamada cinco veces en lugar de una. Nada en su documentación menciona nada sobre esto.

Frustrado, se pone en contacto con el servicio de atención al cliente, que por supuesto está tan sorprendido como usted. Aceptan enviárselo a sus desarrolladores y prometen responderle. Al día siguiente, usted recibe un largo correo electrónico explicando lo que encontraron, que usted envía rápidamente a su jefe.

Aparentemente, los desarrolladores de la compañía de análisis habían estado trabajando en algún código experimental que, bajo ciertas condiciones, reintentaría la llamada de retorno una vez por segundo, durante cinco segundos, antes de fallar con un timeout. Nunca habían tenido la intención de ponerlo en producción, pero de alguna manera lo hicieron, y están totalmente avergonzados y disculpados. Entran en muchos detalles sobre cómo han identificado la avería y qué harán para asegurarse de que no vuelva a ocurrir.

¿Qué sigue?

Usted lo discute con su jefe, pero él no se siente particularmente cómodo con el estado de las cosas. Él insiste, y usted está de acuerdo a regañadientes, en que ya no puede confiar en ellos (eso es lo que le mordió a usted), y que tendrá que averiguar cómo proteger el código de tal vulnerabilidad de nuevo.

Después de algunos retoques, se implementa algún código sobre la exprofeso simple como el siguiente, con el que el equipo parece estar contento:

Nota: Esto debería parecerte familiar de el Capítulo 1, porque estamos esencialmente creando un cerrojo para manejar si hay múltiples invocaciones concurrentes de nuestra devolución de llamada.

Pero entonces uno de sus ingenieros de control de calidad pregunta, «¿qué pasa si nunca llaman al callback?» Oops. Ninguno de los dos había pensado en eso.

Comienzas a seguir por la madriguera de conejo, y piensas en todas las cosas posibles que podrían salir cuando llamen a tu callback. Esta es una lista aproximada de las formas en que la utilidad de análisis podría comportarse mal:

  • Llamar al callback demasiado pronto (antes de que haya sido rastreada)
  • Llamar al callback demasiado tarde (o nunca)
  • Llamar al callback muy pocas o demasiadas veces (¡como el problema que encontraste!)
  • No transmitir ningún entorno/parámetros necesarios a su al callback
  • Comerse cualquier error/excepción que pueda ocurrir

Eso debería parecer una lista preocupante, porque lo es. Probablemente estés empezando a darte cuenta poco a poco de que vas a tener que inventar una gran cantidad de lógica ad hoc en todas y cada una de los callbacks que se pasan a una utilidad en la que no estás seguro de poder confiar.

Ahora te das cuenta un poco más completamente de lo infernal que es el «infierno del callback».

No sólo el código de los demás

Algunos de ustedes pueden ser escépticos en este punto si esto es tan importante como lo estoy haciendo parecer. Tal vez no interactúa con utilidades de terceros, si es que lo hace. Tal vez usted usa APIs versionadas o autohospeda tales librerías, para que su comportamiento no pueda ser cambiado sin su conocimiento.

Así que, contempla esto: ¿puedes confiar realmente en las utilidades que controlas teóricamente (en tu propia base de código)?

Piénsalo de esta manera: la mayoría de nosotros estamos de acuerdo en que al menos hasta cierto punto deberíamos construir nuestras propias funciones internas con algunos controles defensivos sobre los parámetros de entrada, para reducir/prevenir problemas inesperados.

Demasiada confianza en los aportes:

Defensa contra entradas no confiables:

O quizás, aún seguro pero más amigable:

Independientemente de cómo se haga, este tipo de comprobaciones/normalizaciones son bastante comunes en las entradas de funciones, incluso con códigos en los que teóricamente confiamos plenamente. En cierto modo, es como el equivalente en programación del principio geopolítico de «Confiar pero verificar».

Por lo tanto, ¿no es lógico que hagamos lo mismo con la composición de los callbacks de la función asíncrona, no sólo con código verdaderamente externo sino incluso con código que sabemos que generalmente está «bajo nuestro propio control»? Por supuesto que deberíamos.

Pero los callbacks no ofrecen nada para ayudarnos. Tenemos que construir toda esa maquinaria nosotros mismos, y a menudo termina siendo un montón de repetición/sobrecarga que hacemos para cada llamada asíncrona.

El problema más problemático con los callbacks es la inversión del control que conduce a una ruptura completa a lo largo de todas esas líneas de confianza.

Si tienes código que usa callbacks, especialmente pero no exclusivamente con utilidades de terceros, y no estás aplicando algún tipo de lógica de mitigación para todos estos problemas de inversión de la confianza en el control, tu código tiene errores ahora mismo aunque no te hayan mordido todavía. Los bichos latentes siguen siendo bichos.

El infierno, en efecto.

Tratando de Salvar los callbacks


Existen varias variaciones en el diseño de callbacks que han intentado resolver algunos (¡no todos!) de los problemas de confianza que acabamos de analizar. Es un esfuerzo valiente, pero condenado, para evitar que el patrón del callback implosione sobre sí mismo.

Por ejemplo, en lo que respecta a una gestión de errores más elegante, algunos diseños de API proporcionan callbacks divididas (una para la notificación de éxito y otra para la notificación de error):

En las APIs de este diseño, a menudo el manejador de errores failure() es opcional, y si no se proporciona, se asumirá que usted quiere que se traguen los errores. Ugh.

Nota: Este diseño de devolución de llamada dividida es el que utiliza la API de ES6 Promise. Cubriremos ES6 Promises con mucho más detalle en el próximo capítulo.

Otro patrón común de callback es el llamado «estilo de error primero» (a veces llamado «estilo Node», ya que también es la convención utilizada en casi todas las APIs de Node.js), donde el primer argumento de una única devolución de llamada se reserva para un objeto de error (si lo hay). Si tiene éxito, este argumento estará vacío/falso (y cualquier argumento subsiguiente serán los datos de éxito), pero si se está señalando un resultado de error, el primer argumento es set/truthy (se establece a verídico, y normalmente no se pasa nada más):

En ambos casos, deben observarse varias cosas.

En primer lugar, no ha resuelto realmente la mayoría de los problemas de confianza como puede parecer. No hay nada acerca del callback que prevenga o filtre las invocaciones repetidas no deseadas. Además, las cosas están peor ahora, porque puedes obtener tanto señales de éxito como de error, o ninguna, y todavía tienes que codificar en torno a cualquiera de esas condiciones.

Además, no te pierdas el hecho de que si bien es un patrón estándar que puedes emplear, es definitivamente más verboso y repetitivo sin mucha reutilización, así que te vas a cansar de escribir todo eso para cada callback en tu aplicación.

¿Qué pasa con el tema de la confianza de nunca ser llamado? Si esto es una preocupación (¡y probablemente debería serlo!), es probable que necesite establecer un tiempo de espera que cancele el evento. Usted podría hacer una utilidad (sólo se muestra com prueba de concepto) para ayudarle con eso:

Así es como se usa:

Otra cuestión de confianza es ser llamado «demasiado pronto». En términos específicos de la aplicación, esto puede implicar que se llame antes de que se complete alguna tarea crítica. Pero en general, el problema es evidente en las utilidades que pueden invocar el callback que proporciona ahora (sincrónicamente) o más tarde (asincrónicamente).

Este no determinismo alrededor del comportamiento síncrono-o-asíncrono casi siempre va a llevar a errores muy difíciles de rastrear. En algunos círculos, el monstruo de ficción que induce a la locura llamado «Zalgo» se utiliza para describir las pesadillas de sincronía/asincronía. «No sueltes a Zalgo» es un grito común, y conduce a un consejo muy sensato: siempre invoca los callbacks asincrónicamente, incluso si es «inmediatamente» en el siguiente giro del bucle de eventos, para que todas los callbacks sean previsiblemente asíncronos.

Nota: Para más información sobre Zalgo, ver «Don’t Release Zalgo!» de Oren Golan. (https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md) y «Designing APIs for Asynchrony» de Isaac Z. Schlueter (http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony).

Considere:

¿Este código imprimirá 0 (invocación del callback síncrono) o 1 (invocación del callback asíncrono)? Depende… de las condiciones.

Usted puede ver con qué rapidez la imprevisibilidad de Zalgo puede amenazar cualquier programa de JS. Así que la tontería de «nunca liberar a Zalgo» es en realidad un consejo increíblemente común y sólido. Siempre se asíncrono.

¿Qué pasa si no sabes si la API en cuestión siempre ejecutará asíncronamente? Podrías inventar una utilidad como esta prueba de concepto asyncify(..):

Use asyncify(..) de esta manera:

Si la petición Ajax está en la caché y resuelve intentar llamar al callback inmediatamente, o debe ser recuperada por el programa y así completarse más tarde de forma asíncrona, este código siempre devolverá 1 en lugar de 0result(..) no puede evitar ser invocado de forma asíncrona, lo que significa que el a++ tiene la oportunidad de ejecutarse antes de que lo haga result(..).

¡Sí, otra situación marcada como «resuelta»! Pero es ineficiente, y otra vez más recargando su proyecto.

Ésa es la historia, una y otra vez, con callbacks. Pueden hacer casi todo lo que quieras, pero tienes que estar dispuesto a trabajar duro para conseguirlo, y a menudo este esfuerzo es mucho más de lo que puedes o deberías gastar en tal razonamiento de código.

Es posible que desee incorporar APIs u otros mecanismos de lenguaje para resolver estos problemas. Finalmente ES6 ha llegado a la escena con algunas grandes respuestas, así que sigue leyendo!

Revisión


Los callbacks son la unidad fundamental de la asincronía en JS. Pero no son suficientes para el panorama evolutivo de la programación asíncrona a medida que JS madura.

Primero, nuestros cerebros planean las cosas de manera semántica secuencial, bloqueante y con un solo hilo, pero los callbacks expresan el flujo asíncrono de una manera más bien no lineal y no secuencial, lo que hace que sea mucho más difícil razonar adecuadamente sobre dicho código. Un código sobre el que hay que razonar si es malo, es un mal código que provoca errores.

Necesitamos una forma de expresar la asincronía de una manera más síncrona, secuencial y bloqueante, tal como lo hacen nuestros cerebros.

En segundo lugar, y lo que es más importante, los callbacks sufren de inversión de control en el sentido de que implícitamente dan control sobre otra parte (¡a menudo una utilidad de terceros que no está bajo su control!) para invocar la continuación de su programa. Esta transferencia de control nos lleva a una lista preocupante de problemas de confianza, como por ejemplo, si el callback se llama más veces de las que esperamos.

Inventar una lógica ad hoc para resolver estos problemas de confianza es posible, pero es más difícil de lo que debería ser, y produce código más torpe y difícil de mantener, así como código que probablemente esté insuficientemente protegido de estos peligros hasta que los errores lo muerdan visiblemente.

Necesitamos una solución generalizada para todos los problemas de confianza, una que se pueda reutilizar para tantos callbacks como creemos sin recargar innecesariamente nuestro código.

Necesitamos algo mejor que los callbacks. Nos han servido bien hasta ahora, pero el futuro de JavaScript exige patrones de sincronización más sofisticados y capaces. Los capítulos siguientes de este libro se adentrarán en esas evoluciones emergentes.


Capítulo 3: Promesas


En el Capítulo 2, identificamos dos categorías principales de deficiencias en el uso de llamadas de retorno para expresar la asincronía del programa y manejar la concurrencia: la falta de secuencialidad y la falta de confiabilidad. Ahora que entendemos los problemas más detalladamente, es hora de que dirijamos nuestra atención a los patrones que pueden resolverlos.

La cuestión que queremos abordar en primer lugar es la inversión del control, la confianza que se mantiene tan frágilmente y se pierde tan fácilmente.

Recuerde que envolvemos la continuación de nuestro programa en una función callback, y entregamos ese callback a otra parte (potencialmente incluso código externo) y cruzamos los dedos para que haga lo correcto con la invocación del callback.

Hacemos esto porque queremos decir, «esto es lo que pasa después, después de que el paso actual termine.»

Pero, ¿y si pudiéramos desinvertir esa inversión de control? ¿Qué pasa si en vez de entregar la continuación de nuestro programa a otro participante, pudiésemos esperar que nos devolviese una capacidad para saber cuándo termina su tarea, y entonces nuestro código pudiese decidir qué hacer a continuación?

Este paradigma se llama Promesas.

Las promesas están empezando a asaltar el mundo de JS, ya que tanto los desarrolladores como los escritores de especificaciones buscan desesperadamente desenmarañar la locura del callback en su código/diseño. De hecho, la mayoría de las nuevas APIs asíncronas que se están añadiendo a la plataforma JS/DOM se están construyendo sobre Promesas. Así que probablemente sea una buena idea cavar y aprenderlas, ¿no crees?

Nota: La palabra «inmediatamente» se usará frecuentemente en este capítulo, generalmente para referirse a alguna acción de resolución de Promesa. Sin embargo, en prácticamente todos los casos, «inmediatamente» es en relación de términos de comportamiento de la cola de trabajos (véase el capítulo 1), no en el sentido estrictamente síncrono actual.

¿Qué es una promesa?


Cuando los desarrolladores deciden aprender una nueva tecnología o patrón, usualmente su primer paso es «¡Muéstrame el código!» Es muy natural que saltemos con los pies por delante y aprendamos a medida que avanzamos.

Pero resulta que algunas abstracciones se pierden sólo en las APIs. Las promesas son una de esas herramientas en las que puede ser dolorosamente obvio por la forma en que alguien las usa, ya sea que entienda para qué sirven y de qué se trata, en lugar de simplemente aprender y usar la API.

Así que antes de mostrar el código de las Promesas, quiero explicar detalladamente lo que es conceptualmente una Promesa. Espero que esto te guíe mejor a medida que exploras la integración de la teoría de la Promesa en tu propio flujo asíncrono.

Con eso en mente, veamos dos analogías diferentes de lo que es una Promesa.

Valor futuro

Imagínese este escenario: me acerco al mostrador de un restaurante de comida rápida y hago un pedido de una hamburguesa con queso. Le doy al cajero $1.47. Al hacer mi pedido y pagar por él, he hecho una solicitud de devolución de valor (la hamburguesa con queso). He empezado una transacción.

Pero a menudo, la hamburguesa con queso no está disponible inmediatamente para mí. La cajera me da algo en lugar de mi hamburguesa con queso: un recibo con un número de pedido. Este número de pedido es una promesa de IOU («I owe you», un pagaré, un «Se lo debo») que asegura que eventualmente, debo recibir mi hamburguesa con queso.

Así que guardo mi recibo y el número de pedido. Sé que representa mi futura hamburguesa con queso, así que ya no tengo que preocuparme más por ello, ¡aparte de tener hambre!

Mientras espero, puedo hacer otras cosas, como enviar un mensaje de texto a un amigo que dice: «Oye, ¿puedes venir a almorzar conmigo? Voy a comer una hamburguesa con queso».

Ya estoy razonando sobre mi futura hamburguesa con queso, aunque todavía no la tengo en mis manos. Mi cerebro es capaz de hacer esto porque está tratando el número de pedido como un marcador de posición para la hamburguesa con queso. El marcador de posición esencialmente hace que el valor tiempo sea independiente. Es un valor futuro.

Eventualmente, escucho, «¡Pedido 113!» y vuelvo alegremente al mostrador con el recibo en la mano. Le entrego mi recibo a la cajera, y tomo mi hamburguesa a cambio.

En otras palabras, una vez que mi valor futuro estaba listo, cambié mi promesa de valor por el valor mismo.

Pero hay otro posible resultado. Anuncian mi número de pedido, pero cuando voy a buscar mi hamburguesa con queso, la cajera me dice: «Lo siento, pero parece que se nos acabaron las hamburguesas con queso». Dejando de lado por un momento la frustración del cliente ante este escenario, podemos ver una característica importante de los valores futuros: pueden indicar un éxito o un fracaso.

Cada vez que pido una hamburguesa con queso, sé que eventualmente voy a conseguir una hamburguesa con queso, o voy a recibir las tristes noticias de la escasez de hamburguesas con queso, y tendré que pensar en algo más para comer en el almuerzo.

Nota: En el código, las cosas no son tan simples, porque metafóricamente puede que nunca se lanuncie el número de pedido, en cuyo caso nos quedamos indefinidamente en un estado sin resolver. Volveremos a tratar ese caso más tarde.

Valores Ahora y Después

Todo esto puede sonar demasiado abstracto mentalmente como para aplicarlo a su código. Así que seamos más concretos.

Sin embargo, antes de que podamos introducir cómo funcionan las Promesas de esta manera, vamos a derivar en un código que ya entendemos — ¡callbacks! para ver cómo manejar estos valores futuros.

Cuando escribes código para razonar sobre un valor, como hacer matemáticas en un número, te des cuenta o no, has estado asumiendo algo muy fundamental sobre ese valor, que es que ya es un valor concreto:

La operación x + y asume que tanto x como y ya están establecidas. En términos que expondremos en breve, asumimos que los valoresx e y ya están resueltos.

Sería absurdo esperar que el operador + por sí mismo sea de alguna manera mágicamente capaz de detectar y esperar hasta que tanto x como y se resuelvan (es decir preparados), y sólo entonces hacer la operación. Eso causaría caos en el programa si diferentes declaraciones terminaran ahora y otras más tarde, ¿verdad?

¿Cómo es posible que puedas razonar sobre las relaciones entre dos sentencias si una de ellas (o ambas) podría no haber terminado todavía? Si la sentencia 2 se basa en la finalización de la sentencia 1, sólo hay dos resultados: o bien la sentencia 1 ha finalizado ahora mismo y todo procede bien, o bien la sentencia 1 no ha finalizado todavía, y por lo tantola sentencia 2 va a fallar.

Si este tipo de cosas le resultan familiares en el Capítulo 1, ¡bien!

Volvamos a nuestra operación matemática x + y. Imagínate si hubiera una manera de decir: «Agrega x e y, pero si ninguno de los dos aún no está listo, espera a que lo esté». Añádelos tan pronto como puedas».

Tu cerebro podría haber saltado a los callbacks. De acuerdo, entonces…

Tómese sólo un momento para dejar que la belleza (o la falta de ella) de ese fragmento se hunda (silbe pacientemente).

Aunque la fealdad es innegable, hay algo muy importante que notar sobre este patrón asíncrono.

En ese fragmento, tratamos x e y como valores futuros, y expresamos una operación add(..) a la que (desde fuera) no le importa si x o y o ambos están disponibles inmediatamente o no. En otras palabras, normaliza el ahora y el después, de modo que podemos confiar en un resultado predecible de la operación add(..).

Usando un add(..) que es temporalmente consistente — se comporta de la misma manera ahora y después — el código asíncrono es mucho más fácil de razonar.

Para decirlo más claramente: para manejar consistentemente ambos ahora y después, hacemos ambos más tarde: todas las operaciones se convierten en asíncronas.

Por supuesto, este enfoque basado en las llamadas de retorno deja mucho que desear. Es sólo un primer paso diminuto hacia la obtención de los beneficios de razonar sobre los valores futuros sin preocuparse por el aspecto temporal de cuándo está disponible o no.

Promesa de Valor

Definitivamente entraremos en muchos más detalles sobre Promesas más adelante en el capítulo — así que no te preocupes si algo de esto es confuso — pero vislumbremos brevemente cómo podemos expresar el ejemplo x + ya través de Promesas:

Hay dos capas de Promesas en este fragmento.

fetchX() y fetchY() se llaman directamente, y los valores que devuelven (¡promesas!) se pasan a add(...). Los valores subyacentes que esas promesas representan pueden estar listos ahora o después, pero cada promesa normaliza el comportamiento para que sea el mismo a pesar de todo. Razonamos sobre los valores X e Y de forma independiente del tiempo. Son valores futuros.

La segunda capa es la promesa que add(..) crea (a través de Promise.all([ .. ])) y devuelve, la cual esperamos llamando a then(..). Cuando la operación add(..) se completa, nuestro valor futuro de la suma está listo y podemos imprimirlo. Ocultamos dentro de add(..) la lógica de espera de los valores futuros X e Y.

Nota: Dentro de add(..), la llamada Promise.all([ .. ]) crea una promesa (que está a la espera de promiseX y promiseY para resolverse). La llamada encadenada a .then(..) crea otra promesa, que la línea return values[0] + values[1]; resuelve inmediatamente (con el resultado de la suma). Por lo tanto, la llamada then(..) que encadenamos el final de la llamada add(..) — al final del fragmento — en realidad está operando en esa segunda promesa devuelta, en lugar de la primera creada por Promise.all ([ .. ]). También, aunque no estemos encadenando al final de ese segundo then(..), se ha creado otra promesa, y pudiésemos haber elegido observarla/usarla. Este tema del encadenamiento de Promesas será explicado con mucho más detalle más adelante en este capítulo.

Al igual que con los pedidos de hamburguesas con queso, es posible que la resolución de una promesa sea el rechazo en lugar del cumplimiento. A diferencia de una Promesa cumplida, donde el valor es siempre programático, un valor de rechazo — comúnmente llamado «razón de rechazo» — puede ser fijado directamente por la lógica del programa, o puede resultar implícitamente de una excepción de tiempo de ejecución.

Con la promesas, la llamada then(..) puede tener dos funciones, la primera para el cumplimiento (como se mostró anteriormente), y la segunda para el rechazo:

Si algo salió mal al obtener X o Y, o algo de alguna manera falló durante la adición, la promesa que add(..)devuelve es rechazada, y el segundo manejador de errores de devolución de llamada pasado a then(..) recibirá el valor de rechazo de la promesa.

Debido a que las Promesas encapsulan el estado dependiente del tiempo — esperando el cumplimiento o rechazo del valor subyacente — desde el exterior, la Promesa en sí es independiente del tiempo, y por lo tanto las Promesas pueden ser compuestas (combinadas) en formas predecibles sin importar el tiempo o el resultado subyacente.

Además, una vez que una Promesa se resuelve, permanece así para siempre – se convierte en un valor inmutable en ese momento – y puede ser observada tantas veces como sea necesario.

Nota: Debido a que una Promesa es inmutable externamente una vez resuelta, ahora es seguro pasar ese valor a cualquier parte y saber que no puede ser modificada accidental o maliciosamente. Esto es especialmente cierto en relación con las múltiples partes que observan la resolución de una Promesa. No es posible que una de las partes afecte la capacidad de la otra parte para cumplir con la resolución de la Promesa. La inmutabilidad puede sonar como un tema académico, pero en realidad es uno de los aspectos más fundamentales e importantes del diseño de Promesa, y no debería pasarse por alto casualmente.

Ese es uno de los conceptos más poderosos e importantes para entender las Promesas. Con una buena cantidad de trabajo, podría crear ad hoc los mismos efectos con nada más que una fea composición de devolución de llamada, pero esa no es realmente una estrategia efectiva, especialmente porque tienes que hacerlo una y otra vez.

Las promesas son un mecanismo fácilmente repetible para encapsular y componer valores futuros.

Evento de finalización

Como acabamos de ver, una Promesa individual se comporta como un valor futuro. Pero hay otra manera de pensar en la resolución de una Promesa: como un mecanismo de control de flujo – un esto-luego-eso temporal – para dos o más pasos en una tarea asíncrona.

Imaginemos llamar a una función foo(..) para realizar alguna tarea. No conocemos ninguno de sus detalles, ni nos importa. Puede completar la tarea de inmediato o puede tomar un tiempo.

Simplemente necesitamos saber cuándo termina foo(..) para poder pasar a nuestra siguiente tarea. En otras palabras, nos gustaría que se nos notificara de la finalización de foo(..) para que podamos continuar.

En la modalidad típica de JavaScript, si necesita escuchar una notificación, lo más probable es que lo piense en términos de eventos. Por lo tanto, podríamos replantear nuestra necesidad de notificación como una necesidad de escuchar la finalización (o continuación) de un evento emitido por foo(..).

Nota: Si lo llama «evento de finalización» o «evento de continuación» depende de su perspectiva. ¿Se centra más en lo que pasa con foo(..), o en lo que pasa después de que foo(..) termina? Ambas perspectivas son precisas y útiles. La notificación de eventos nos dice que foo(..) se ha completado, pero también que está bien continuar con el siguiente paso. De hecho, el callback que usted para ser llamado por el evento de notificación es en sí mismo lo que previamente hemos llamado una continuación. Debido a que el evento de finalización está un poco más enfocado en foo(..), que necesita más atención en este momento, estamos ligeramente a favor de la opción «evento de finalización» para el resto de este texto.

Con los callbacks, la «notificación» sería nuestro callback invocada por la tarea (foo(..)). Pero con Promises, damos la vuelta a la relación, y esperamos que podamos escuchar un evento de foo(..), y cuando se nos notifica, proceder en consecuencia.

Primero, considera un pseudocódigo:

Llamamos a foo(...) y luego configuramos a dos oyentes de eventos, uno para «finalización» y otro para «error» — los dos posibles resultados finales de la llamada a foo(...). En esencia, foo(...) ni siquiera parece ser consciente de que el código de llamada se ha suscrito a estos eventos, lo que hace una muy buena separación de preocupaciones.

Desafortunadamente, tal código requeriría un poco de «magia» del entorno JS que no existe (y probablemente sería un poco poco poco práctico). Esta es la manera más natural de expresarlo en JS:

foo(...) crea expresamente una capacidad de suscripción de evento para volver atrás, y el código de llamada recibe y registra a los dos manejadores de eventos sobre ella.

La inversión del código normal orientado a callbacks debería ser obvia, y es intencional. En lugar de pasar el callback a foo(...), devuelve una capacidad de evento que llamamos evt, que recibe el callback.

Pero si usted recuerda del Capítulo 2, los callbacks en sí mismos representan una inversión de control. Por lo tanto, invertir el patrón callback es en realidad una inversión de inversión o una desinversión de control, restaurando el control al código de llamada donde queríamos que estuviera en primer lugar.

Un beneficio importante es que múltiples partes separadas del código pueden tener la capacidad de escuchar eventos, y todas ellas pueden ser notificadas independientemente de cuando foo(...) se complete para realizar los pasos subsiguientes después de su finalización:

La desinversión del control permite una mejor separación de las preocupaciones, donde bar(...) y baz(...)no necesitan estar involucrados en cómo se llama foo(...). Del mismo modo, foo(...) no necesita saber o preocuparse de que bar(...) y baz(...) existan o estén esperando a ser notificados cuando foo(...)termine.

Esencialmente, este objeto evt es una tercera pate neutral en una negociación entre los distintos actores.

Eventos de Promesas

Como ya habrás adivinado, la capacidad de escucha de eventos evt es una analogía de una Promesa.

En un enfoque basado en Promesa, el fragmento anterior tendría foo(...) creando y devolviendo una instancia de Promesa, y esa promesa sería pasada a bar(...) y baz(...).

Nota: Los «eventos» de resolución de Promesas que escuchamos no son estrictamente eventos (aunque ciertamente se comportan como eventos para estos propósitos), y no son típicamente llamados «finalización» o «error». En su lugar, usamos then(...) para registrar un evento «entonces». O quizás más precisamente, then(...) registra eventos de «cumplimiento» y/o «rechazo», aunque no vemos esos términos usados explícitamente en el código.

Considere:

Nota: El patrón mostrado con new Promise( funtion(...){...} ) es generalmente llamado el «constructor revelador». La función pasada se ejecuta inmediatamente (no se aplaza la sincronización, como se hace con los callbacks a then(...)), y se le proporcionan dos parámetros, que en este caso hemos denominado resolve y reject. Estas son las funciones de resolución para la promesa. resolve(...) generalmente señala el cumplimiento, y reject(...) señala el rechazo.

Probablemente puedas adivinar cómo podrían ser los componentes internos de bar(...) y baz(...):

La resolución de promesas no necesariamente tiene que implicar el envío de un mensaje, como lo hizo cuando estábamos examinando las Promesas como valores futuros. Puede ser sólo una señal de control de flujo, como se usó en el fragmento anterior.

Otra forma de enfocar esto es:

Nota: Si has visto antes la codificación basada en Promesa, podrías estar tentado a creer que las dos últimas líneas de ese código podrían escribirse como p.then(...).then(...), usando encadenamiento, en lugar de p.then(...); p.then(...). Eso tendría un comportamiento completamente diferente, ¡así que tenga cuidado! Puede que la diferencia no esté clara ahora mismo, pero en realidad es un patrón de sincronización diferente al que hemos visto hasta ahora: división/bifurcación. No te preocupes! Volveremos a este punto más adelante en este capítulo.

En vez de pasar la promesa p a bar(...) y baz(...), usamos la promesa de controlar de cuando bar(...) y baz(...) serán ejecutados, si es que alguna vez lo son. La principal diferencia está en el manejo de errores.

En la primera aproximación, bar(...) se llama independientemente de si foo(...) tiene éxito o falla, y maneja su propia lógica de seguridad si se le notifica que foo(...) falló. Lo mismo es cierto para baz(...), obviamente.

En el segundo fragmento, bar(...) sólo se llama si foo(...) tiene éxito, y de lo contrario oopsBar(...) se llama. Lo mismo para baz(...).

Ninguno de los dos enfoques es correcto per se. Habrá casos en los que se prefiera uno sobre el otro.

En cualquier caso, la promesa p que vuelve de foo(...) se usa para controlar lo que sucede después.

Además, el hecho de que ambos fragmentos terminen llamando a then(...) dos veces sobre la misma promesa p ilustra el punto descrito anteriormente, que es que las Promesas (una vez resueltas) conservan su misma resolución (cumplimiento o rechazo) para siempre, y pueden ser observadas posteriormente tantas veces como sea necesario.

Siempre que se resuelva p, el siguiente paso siempre será el mismo, tanto ahora como después.

then con «tipado de patos»(duck typing)


Nota de la traducción, recordar la anañogia del tipado de patos: «Cuando veo un ave que camina como un pato, nada como un pato y suena como un pato, a esa ave yo la llamo un pato.» https://es.wikipedia.org/wiki/Duck_typing

En la tierra de las promesas, un detalle importante es cómo saber con seguridad si algún valor es una promesa genuina o no. O más directamente, ¿es un valor que se comportará como una Promesa?

Dado que las Promesas se construyen a partir de la sintaxis new Promise(..), se podría pensar que p instanceof Promise sería una comprobación aceptable. Pero desafortunadamente, hay un número de razones que no son totalmente suficientes.

Principalmente, puede recibir un valor Promise de otra ventana del navegador (iframe, etc.), que tendría su propia Promise diferente de la que se encuentra en la ventana/marco actual, y esa comprobación no identificaría la instancia Promise.

Además, una biblioteca o framework puede optar por incluir sus propias Promesas y no utilizar la implementación nativa de la Promesa ES6 para hacerlo. De hecho, es muy posible que esté utilizando Promises con bibliotecas de navegadores antiguos que no tienen Promise en absoluto.

Cuando discutamos los procesos de resolución de Promesas más adelante en este capítulo, será más obvio por qué un valor no-genuino-pero-parecido-a-una-promesa todavía sería muy importante para ser capaz de reconocer y asimilar. Pero por ahora, créeme que es una pieza crítica del rompecabezas.

Como tal, se decidió que la manera de reconocer una Promesa (o algo que se comporta como una Promesa) sería definir algo llamado «thenable» como cualquier objeto o función que tenga un método then(...). Se asume que cualquiera de estos valores es un valor que cumple con la Promesa.

El término general para «comprobaciones de tipo» que hacen suposiciones sobre el «tipo» de un valor basado en su forma (qué propiedades están presentes) se llama «duck typing» — «Si parece un pato, y grazna como un pato, debe ser un pato» (ver el título de Tipos y Gramática de esta serie de libros). Así que la comprobación de tipado de patos para un thenable sería más o menos:

¡Qué asco! Dejando a un lado el hecho de que esta lógica es un poco fea de implementar en varios lugares, hay algo más profundo y preocupante en marcha.

Si intentas cumplir una Promesa con cualquier valor de objeto/función que tenga una función then(...), pero no pretendías que fuera tratada como una Promesa, no tienes suerte, porque automáticamente será reconocida como thenable y tratada con reglas especiales (ver más adelante en el capítulo).

Esto es incluso cierto si no te das cuenta de que el valor tiene un then(...) en él. Por ejemplo:

v no parece en absoluto una Promesa o que sea»thenable». Es sólo un objeto sencillo con algunas propiedades. Probablemente estás intentando enviar ese valor como cualquier otro objeto.

Pero sin que lo sepas, v también está enlazado vía [[Prototype]] (ver el título de este & Prototipos de Objeto de esta serie de libros) a otro objeto o, el cual tiene un then(...) en él. Así que el «tipado de patos thenable» pensará y asumirá que v es un thenable. Uh oh.

Ni siquiera tiene que ser algo tan directamente intencional como eso:

Se asumirá que tanto v1 como v2 son thenables. Usted no puede controlar o predecir si cualquier otro código accidental o maliciosamente añade then(..) a Object.prototype, Array.prototype, o cualquiera de los otros prototipos nativos. Y si lo que se especifica es una función que no llama a ninguno de sus parámetros como callbacks, entonces cualquier Promesa resuelta con tal valor se quedará para siempre en el olvido! Loco.

¿Suena inverosímil o improbable? Tal vez.

Pero ten en cuenta que antes de ES6 existían en la comunidad varias bibliotecas conocidas que no inclían Promesas y que ya tenían un método llamado then(...). Algunas de esas librerías eligieron renombrar sus propios métodos para evitar colisiones (¡eso apesta!). Otros simplemente han sido relegados al desafortunado estatus de «incompatibles con la codificación basada en Promesa» en recompensa por su incapacidad de cambiar para salir del camino.

La decisión de los estándares de secuestrar el nombre de propiedad then que antes no estaba reservado — y parecer como completamente polivalentes — significa que ningún valor (o cualquiera de sus delegados), ya sea pasado, presente o futuro, puede tener una función de then(..) presente, ya sea a propósito o por accidente, o que el valor se confundirá con un thenable en los sistemas de Promisas, que probablemente creará errores que son realmente difíciles de rastrear.

Advertencia: No me gusta cómo terminamos con el tipado de patos de thenables para el reconocimiento de Promesas. Había otras opciones, como el «marcado» o incluso el «antimarcado»; lo que tenemos parece un compromiso en el peor de los casos. Pero no todo es pesimismo. El tipado de patos thenable puede ser útil, como veremos más adelante. Sólo ten cuidado de que el tipado de patos puede ser peligroso si identifica incorrectamente algo como una Promesa que no lo es.

Confianza de la Promesa


Ahora hemos visto dos fuertes analogías que explican diferentes aspectos de lo que las Promesas pueden hacer por nuestro código asíncrono. Pero si nos detenemos ahí, nos habremos perdido quizás la característica más importante que establece el patrón de la Promesa: la confianza.

Mientras que los valores futuros y las analogías de los eventos de finalización aparecen explícitamente en los patrones de código que hemos explorado, no será del todo obvio por qué, o por cómo, las Promesas están diseñadas para resolver todos los problemas de inversión de la confianza en el control que presentamos en la sección «Problemas de confianza» del Capítulo 2. Pero con un poco de investigación, podemos descubrir algunas garantías importantes que restauran la confianza en el código asíncrono que el Capítulo 2 derribó!

Comencemos por repasar los problemas de confianza con la codificación de sólo llamadas de retorno. Cuando pasas un callback a foo(...), puede que suceda:

  • Llamar al callback demasiado pronto
  • Llamar al callback demasiado tarde (o nunca)
  • Llamar al callback muy pocas o demasiadas veces
  • Error al pasar entorno/parámetros necesarios
  • Comerse cualquier error/excepción que pueda ocurrir

Las características de las Promesas están intencionalmente diseñadas para proporcionar respuestas útiles y repetibles a todas estas preocupaciones.

Llamar demasiado pronto

Principalmente, esto es una preocupación de si el código puede introducir efectos similares a los de Zalgo (ver Capítulo 2), donde a veces una tarea termina sincrónicamente y a veces asincrónicamente, lo que puede llevar a condiciones de carrera.

Las promesas por definición no pueden ser susceptibles a esta preocupación, porque incluso una Promesa cumplida inmediatamente (como new Promise (function(resolve){ resolve(42); })) no puede ser observada sincrónicamente.

Es decir, cuando usted llama a then(...) en una Promesa, incluso si esa Promesa ya estaba resuelta, el callback que usted proporciona a then(...) siempre será llamada asincrónicamente (para más información sobre esto, refiérase a «Trabajos» en el Capítulo 1).

Ya no hay necesidad de insertar sus propios hacks setTimeout(...,0). Las promesas previenen a Zalgo automáticamente.

Llamar demasiado tarde

Al igual que en el punto anterior, las llamadas de observación registradas de una Promesa se programan automáticamente cuando la capacidad de creación de la Promesa llama a resolve(...) o reject(...). Los callbacks programados se dispararán previsiblemente en el siguiente momento asíncrono (véase «Trabajos» en el capítulo 1).

No es posible para la observación síncrona, así que no es posible que una cadena síncrona de tareas se ejecute de tal manera que, en efecto, «retrase» otro callback para que no suceda como se esperaba. Es decir, cuando se resuelve una Promesa, todos callbacks registrados en ella serán llamados, en orden, inmediatamente en la siguiente oportunidad asíncrona (de nuevo, ver «Trabajos» en el Capítulo 1), y nada de lo que ocurra dentro de una de esos callbacks puede afectar/demorar la llamada de otros callbacks.

Por ejemplo:

Aquí, "C" no puede interrumpir y preceder a "B", en virtud de cómo se definen las Promesas para operar.

Rarezas de programación de promesas

Es importante tener en cuenta, sin embargo, que hay muchos matices de programación donde el orden relativo entre los callbacks encadenados de dos Promesas separadas no es confiable y predecible.

Si dos promesas p1 y p2 ya están resueltas, debería ser cierto que p1.then(...); p2.then(...) terminaría llamando al/los callback/s para p1 antes que a los de p2. Pero hay casos sutiles en los que eso podría no ser cierto, como los siguientes:

Cubriremos esto más adelante, pero como puedes ver, p1 no se resuelve con un valor inmediato, sino con otra promesa p3 que a su vez se resuelve con el valor "B". El comportamiento especificado es «desenvolver» p3 en p1, pero de forma asíncrona, de modo que la devolución de llamada de p1 está detrás de la devolución de llamada de p2 en la cola de trabajos asíncrona (consulte el Capítulo 1).

Para evitar tales pesadillas de matices, nunca debes confiar en nada sobre el orden/programación de devoluciones de llamadas a través de Promesas. De hecho, una buena práctica es no codificar de tal manera que el orden de múltiples callbacks importe en absoluto. Evita eso si puedes.

Nunca llamar al callback

Esta es una preocupación muy común. Es abordable de varias maneras con Promesas.

Primero, nada (ni siquiera un error de JS) puede impedir que una Promesa te notifique de su resolución (si está resuelta). Si registras tanto el cumplimiento como el rechazo de los callbacks de una Promesa, y la Promesa se resuelve, una de los dos callbacks siempre será llamado.

Por supuesto, si tus callbacks tienen errores JS, puede que no veas el resultado que esperas, pero el callback de hecho habrá sido llamado. Cubriremos más adelante cómo ser notificado de un error en tu callback, incluso aquellos que no son aceptados.

¿Pero qué pasa si la Promesa misma nunca se resuelve de una u otra manera? Incluso esa es una condición para la cual las Promesas proveen una respuesta, usando una abstracción de nivel superior llamada «carrera»:

Hay más detalles a considerar con este patrón de tiempo de espera de Promise, pero volveremos a ello más adelante.

Lo importante esque podemos asegurar una señal sobre el resultado de foo() para evitar que cuelgue nuestro programa indefinidamente.

Demasiadas llamadas o demasiado pocas llamadas

Por definición, uno es el número apropiado de veces para llamar al callback. El caso «demasiado pocas» serían cero llamadas, que es lo mismo que el caso «nunca» que acabamos de examinar.

El caso «demasiadas» es fácil de explicar. Las promesas se definen de manera que sólo puedan ser resueltas una vez. Si por alguna razón el código de creación de la Promesa intenta llamar a resolve(...) o reject(...)varias veces, o intenta llamar a ambos, la Promesa aceptará sólo la primera resolución, e ignorará silenciosamente cualquier intento posterior.

Debido a que una Promesa sólo puede ser resuelta una vez, cualquier callback then() registrado sólo se llamará una vez (cada vez).

Por supuesto, si registra el mismo callback más de una vez, (por ejemplo, p.then(f); p.then(f);), se llamará tantas veces como se registró. La garantía de que una función de respuesta se llama sólo una vez no le impide dispararse a sí mismo en el pie.

No pasar ningún parámetro/entorno

Las promesas pueden tener, como máximo, un valor de resolución (cumplimiento o rechazo).

Si no se resuelve explícitamente con un valor de cualquier manera, el valor es undefined, como es típico en JS. Pero cualquiera que sea el valor, siempre se pasará a todas los callbacks registrados (y pertinentes: cumplimiento o rechazo), ya sea ahora o en el futuro.

Algo a lo que hay que estar atentos: Si llama a resolve(...) o reject(...) con múltiples parámetros, todos los parámetros posteriores al primero serán ignorados silenciosamente. Aunque esto pueda parecer una violación de la garantía que acabamos de describir, no lo es exactamente, porque constituye un uso inválido del mecanismo de la Promesa. Otros usos inválidos de la API (como llamar a resolve(...) varias veces) están igualmente protegidos, por lo que el comportamiento de Promise aquí es consistente (si no un poco frustrante).

Si desea pasar varios valores, debe envolverlos en otro valor único que los pase, como un array o un objeto.

En cuanto al entorno, las funciones en JS siempre conservan su cierre del ámbito en el que están definidas (ver el título Scope & Closures de esta serie), por lo que, por supuesto, seguirían teniendo acceso a cualquier estado circundante que usted proporcione. Por supuesto, lo mismo ocurre con el diseño del callback, por lo que no se trata de un aumento específico de los beneficios de Promises, pero es una garantía en la que podemos confiar de todos modos.

Recepción de Errores/Excepciones

En el sentido básico, esto es una reafirmación del punto anterior. Si usted rechaza una Promesa con una razón (o lo que es lo mismo, un mensaje de error), ese valor se pasa a el/los callback/s de rechazo.

Pero hay algo mucho más grande en juego aquí. Si en cualquier momento en la creación de una Promesa, o en la observación de su resolución, ocurre una excepción de JS, tal como un TypeError o ReferenceError, esa excepción será capturada, y obligará a que la Promesa en cuestión sea rechazada.

Por ejemplo:

La excepción JS que ocurre en foo.bar() se convierte en un rechazo de la Promesa que puedes atrapar y a la que puedes responder.

Este es un detalle importante, porque resuelve efectivamente otro momento potencial «Zalgo», que es que los errores podrían crear una reacción síncrona mientras que los no errores serían asíncronos. Las promesas convierten incluso las excepciones de JS en comportamiento asíncrono, reduciendo así las posibilidades de que se produzcan condiciones de carrera.

Pero, ¿qué sucede si se cumple una Promesa, pero hay un error de excepción de JS durante la observación (en un callback registrado then(...))? Incluso esos no están perdidos, pero puede que te sorprenda un poco cómo se manejan, hasta que profundices un poco más:

Espera, eso hace que parezca que la excepción de foo.bar() realmente fue tragada. No temas, no lo hizo. Pero algo más profundo está mal, que es que no lo hemos escuchado. La llamada p.then(...) devuelve otra promesa, y es esa promesa la que será rechazada con la excepción TypeError.

¿Por qué no podría simplemente llamar al gestor de errores que hemos definido allí? Parece un comportamiento lógico en la superficie. Pero violaría el principio fundamental de que las Promesas son inmutables una vez resueltas. p ya se cumplió con el valor 42, por lo que no se puede cambiar más tarde a un rechazo sólo porque hay un error en la observación de la resolución de p.

Además de la violación del principio, tal comportamiento podría causar estragos, si se dice que hubo múltiples llamadas registradas then(...) sobre la promesa p, porque algunas serían llamadas y otras no, y sería muy opaco en cuanto a por qué.

¿Promesa confiable?

Hay un último detalle que examinar para establecer la confianza basada en el patrón de la Promesa.

Sin duda has notado que las Promesas no se deshacen de los callbacks en absoluto. Sólo cambian el lugar donde se pasa el callback. En vez de pasarle un callback a foo(...), recibimos algo (ostensiblemente una Promesa genuina) de foo(...), y en su lugar le pasamos la llamada a ese algo.

Pero, ¿por qué sería esto más confiable que un simple callback? ¿Cómo podemos estar seguros de que lo que recibimos es de hecho una promesa confiable? ¿No es básicamente todo un castillo de naipes en el que podemos confiar sólo porque ya hemos confiado?

Uno de los detalles más importantes, pero a menudo pasados por alto, de las Promesas es que también tienen una solución para este problema. Incluido con la implementación nativa Promise de ES6 está Promise.resolve(...).

Si le pasas un valor inmediato, no una Promesa, sin posibilidad de cambio a Promise.resolve(...), obtienes una promesa que se cumple con ese valor. En otras palabras, estas dos promesas p1 y p2 se comportarán básicamente de la misma manera:

Pero si usted pasa una Promesa a Promise.resolve(...), usted sólo obtiene la misma promesa de vuelta:

Aún más importante, si le pasas un valor no promesa a Promise.resolve(...), intentará desenvolver ese valor, y el desenvolvimiento continuará hasta que se extraiga un valor final concreto que no sea de tipo promesa.

¿Recuerda nuestra discusión anterior sobre los «thenables»?

Considere:

p es un thenable, pero no es una Promesa genuina. Por suerte, es razonable, como la mayoría lo será. ¿Pero qué tal si en vez de eso vuelves con algo como:

p es un thenable, pero no se comporta tan bien como una promesa. ¿Es malicioso? ¿O simplemente ignora cómo deberían funcionar las Promesas? Realmente no importa, para ser honesto. En cualquier caso, no es confiable tal como está.

No obstante, podemos pasar cualquiera de estas versiones de p a Promise.resolve(...), y obtendremos el resultado normalizado y seguro que esperamos:

Promise.resolve(...) aceptará cualquier thenable, y lo desenvolverá a su valor no-thenable. Pero tú vuelves de Promise.resolve(...) una Promesa real y genuina en su lugar, en la que puedes confiar. Si lo que has pasado ya es una Promesa genuina, simplemente la recuperas, así que no hay nada malo en filtrar a través de Promise.resolve(...) para ganar confianza.

Así que digamos que estamos llamando a una utilidad foo(...) y no estamos seguros de que podamos confiar en que su valor de retorno sea una Promesa correcta, pero sabemos que al menos es una promesa que se puede cumplir. Promise.resolve(...) nos dará un envoltorio de Promesa confiable para encadenar:

Nota: Otro efecto secundario beneficioso de envolver Promise.resolve(...) alrededor del valor de retorno de cualquier función (thenable o no) es que es una manera fácil de normalizar esa llamada de función en una tarea asíncrona correcta. Si foo(42) devuelve un valor inmediato a veces, o una Promesa otras veces, Promise.resolve( foo(42)) se asegura de que siempre de como resultado una Promesa. Y evitar a Zalgo haciendo que el código sea mucho mejor.

Confianza Construida

Esperemos que la discusión anterior ahora «resuelva» completamente en tu mente por qué la Promesa es confiable y, lo que es más importante, por qué esa confianza es tan crítica en la construcción de software robusto y mantenible.

¿Se puede escribir código asíncrono en JS sin confianza? Por supuesto que puedes. Nosotros, los desarrolladores de JS, hemos estado codificando asíncronamente con nada más que callbacks durante casi dos décadas.

Pero una vez que empiezas a cuestionarte cuánto puedes confiar en los mecanismos en los que te basas para ser realmente predecible y confiable, empiezas a darte cuenta de que los callbacks tienen una base de confianza bastante inestable.

Las promesas son un patrón que mejora los callbacks con semántica confiable, para que el comportamiento sea más razonable y confiable. Desinvirtiendo la inversión del control de callbacks, colocamos el control con un sistema confiable (Promises) que fue diseñado específicamente para traer cordura a nuestra asincronía.

Flujo de cadena


Ya hemos insinuado esto un par de veces, pero las Promesas no son sólo un mecanismo para un solo paso para una operación tipo esto-entonces-aquello. Ese es el bloque de construcción, por supuesto, pero resulta que podemos enlazar múltiples Promesas para representar una secuencia de pasos asíncronos.

La clave para que esto funcione se basa en dos comportamientos intrínsecos a las Promesas:

  • Cada vez que usted llama a then(...) en una Promesa, crea y devuelve una nueva Promesa, con la cual podemos encadenar.
  • Cualquiera que sea el valor que devuelvas de la llamada del callback de cumplimiento then(...) (el primer parámetro) se establece automáticamente como el cumplimiento de la Promesa encadenada (desde el primer punto).

Primero vamos a ilustrar lo que eso significa, y luego derivaremos cómo nos ayuda a crear secuencias de control de flujo asíncronas. Considere lo siguiente:

Devolviendo v * 2 (por ejemplo, 42), cumplimos la promesa p2 que la primera llamada then(...) creó y devolvió. Cuando la llamada de p2 then(...) se ejecuta, está recibiendo el cumplimiento de la declaración return v * 2. Por supuesto, p2.then(...) crea otra promesa más, que podríamos haber almacenado en una variable p3.

Pero es un poco molesto tener que crear una variable intermedia p2 (o p3, etc.). Afortunadamente, podemos encadenarlos fácilmente:

Así que ahora el primer then(...) es el primer paso en una secuencia asíncrona, y el segundo then(...) es el segundo paso. Esto podría continuar durante todo el tiempo que fuera necesario para extenderlo. Simplemente sigue encadenando un then(...) anterior con cada Promesa creada automáticamente.

Pero hay algo que falta aquí. ¿Y si queremos que el paso 2 espere a que el paso 1 haga algo asíncrono? Estamos utilizando una declaración de devolución inmediata, que cumple inmediatamente la promesa encadenada.

La clave para hacer que una secuencia Promise verdaderamente asíncrona sea capaz en cada paso es recordar cómo funciona Promise.resolve(...) cuando lo que se le pasa es una Promise o un valor final. Promise.resolve(...) devuelve directamente una Promesa genuina recibida, o desenvuelve el valor de un thenable recibido — y continúa recursivamente mientras desenvuelve thenables.

El mismo tipo de desenvolvimiento ocurre si usted devuelve una promesa del manejador de cumplimiento (o rechazo). Considere:

A pesar de que envolvimos 42 en una promesa que devolvemos, se desenvolvió y terminó como la resolución de la promesa encadenada, de tal manera que el segundo then(...) siguió recibiendo 42. Si introducimos la asincronía a esa promesa de envoltura, todo sigue funcionando bien de la misma manera:

¡Eso es increíblemente poderoso! Ahora podemos construir una secuencia de cuantos pasos de sincronización queramos, y cada paso puede retrasar el siguiente paso (¡o no!), según sea necesario.

Por supuesto, el valor que pasa de un paso a otro en estos ejemplos es opcional. Si no se devuelve un valor explícito, se asume un undefined implícito, y las promesas siguen encadenadas de la misma manera. Por lo tanto, cada resolución de Promesa es sólo una señal para proceder al siguiente paso.

Para ampliar la ilustración del encadenado, vamos a generalizar una creación de promesa de retardo (sin mensajes de resolución) en una utilidad que podemos reutilizar para múltiples pasos:

Llamando a delay(200) se crea una promesa que se cumplirá en 200ms, y luego devolvemos esa promesa desde el primer callback then(...) completado, lo que causa que la segunda promesa then(...) espere esa promesa de 200ms.

Nota: Como se ha descrito, técnicamente hay dos promesas en ese intercambio: la promesa de 200ms de retardo y la segunda promesa then(...) que se encadena. Pero puede que te resulte más fácil combinar mentalmente estas dos promesas, porque el mecanismo de la Promesa fusiona automáticamente sus estados para ti. A este respecto, se podría pensar en return delay(200) como la creación de una promesa que sustituye a la promesa encadenada devuelta anteriormente.

Para ser honesto, sin embargo, las secuencias de retardos sin pasar mensajes no son un ejemplo terriblemente útil de control de flujo de Promise. Veamos un escenario que es un poco más práctico.

En lugar de temporizadores, consideremos hacer peticiones de Ajax:

Primero definimos una utilidad request(...) que construye una promesa para representar la finalización de la llamada ajax(...):

Nota: Los desarrolladores suelen encontrarse con situaciones en las que quieren realizar un control de flujo asíncrono consciente de Promise con utilidades que no están habilitadas para Promise (como ajax(...) aquí, que espera una devolución de llamada). Aunque el mecanismo nativo ES6 Promise no resuelve automáticamente este patrón para nosotros, prácticamente todas las librerías Promise lo hacen. Suelen llamar a este proceso » elevación» o » promisorio» o alguna variación del mismo. Volveremos a esta técnica más tarde.

Usando la devolución de promesa request(...), creamos el primer paso en nuestra cadena implícitamente llamándola con la primera URL, y encadenando esa promesa devuelta con el primer then(...).

Una vez que la respuesta1 vuelve, usamos ese valor para construir una segunda URL, y hacer una segunda llamada request(...). La promesa del segundo request(...) se devuelve para que el tercer paso de nuestro control de flujo asíncrono espere a que se complete la llamada de Ajax. Finalmente, imprimimos response2 una vez que regresa.

La cadena Promise que construimos no sólo es un control de flujo que expresa una secuencia de sincronización de varios pasos, sino que también actúa como un canal de mensajes para propagar mensajes de un paso a otro.

¿Qué pasa si algo sale mal en uno de los pasos de la cadena de la Promesa? Hay un error/excepción por Promesa, lo que significa que es posible capturar tal error en cualquier punto de la cadena, y que la captura actúa como una especie de «restablecimiento» de la cadena a su operación normal en ese punto:

Cuando el error se produce en el paso 2, el controlador de rechazo del paso 3 lo detecta. El valor de retorno (42 en este fragmento), si lo hay, de ese manejador de rechazo cumple la promesa para el siguiente paso (4), de modo que la cadena vuelve a estar en estado de cumplimiento.

Nota: Como discutimos anteriormente, cuando se devuelve una promesa de un manejador de cumplimiento, se desenvuelve y puede retrasar el siguiente paso. Esto también es cierto para las promesas de los manejadores de rechazo, de tal manera que si la devolución 42 en el paso 3 en su lugar devolviera una promesa, esa promesa podría retrasar el paso 4. Una excepción lanzada dentro del manejador de cumplimiento o rechazo de una llamada then(...) causa que la siguiente promesa (encadenada) sea inmediatamente rechazada con esa excepción.

Si usted llama then(...) en una promesa, y usted pasa solamente un manejador del cumplimiento a él, un manejador asumido del rechazo se substituye:

Como puede ver, el manejador de rechazo asumido simplemente relanza el error, lo que termina forzando a p2 (la promesa encadenada) a rechazar con la misma razón de error. En esencia, esto permite que el error continúe propagándose a lo largo de una cadena Promise hasta que se encuentre un manejador de rechazo explícitamente definido.

Nota: Más adelante cubriremos más detalles sobre el manejo de errores con Promises, porque hay otros detalles sutiles de los que hay que preocuparse.

Si no se pasa una función válida apropiada como parámetro del manejador de cumplimiento a then(...), también hay un manejador por defecto sustituido:

Como puede ver, el manejador de cumplimiento por defecto simplemente pasa cualquier valor que reciba al siguiente paso (Promise).

Nota: El patrón then(null,function(err){...}) — sólo maneja los rechazos (si los hay) pero dejando pasar los cumplimientos — tiene un atajo en la API: catch(function(err){... }). Cubriremos catch(...) más completamente en la siguiente sección.

Repasemos brevemente los comportamientos intrínsecos de Promesas que permiten encadenar el control de flujo:

  • Una llamada then(...) contra una Promesa produce automáticamente una nueva Promesa para devolver desde la llamada.
  • Dentro de los manejadores de cumplimiento/rechazo, si usted devuelve un valor o se lanza una excepción, la nueva Promesa devuelta (encadenable) se resuelve en consecuencia.
  • Si el controlador de cumplimiento o rechazo devuelve una Promesa, se desenvuelve, de modo que cualquiera que sea su resolución se convertirá en la resolución de la Promesa encadenada devuelta del actual then(...).

Si bien el control del flujo de encadenamiento es útil, probablemente sea más preciso considerarlo como un beneficio secundario de la forma en que las Promesas se componen (combinan), en lugar de la intención principal. Como ya hemos discutido en detalle varias veces, las promesas normalizan la asincronía y encapsulan el estado de valor dependiente del tiempo, y eso es lo que nos permite encadenarlas juntas de esta manera útil.

Ciertamente, la expresividad secuencial de la cadena (this-then-this-then-this…) es una gran mejora sobre el enrevesado desorden de llamadas de retorno como lo identificamos en el Capítulo 2. Pero todavía hay una buena cantidad de repetición (then(...) y function(){...} ) que tratar. En el siguiente capítulo, veremos un patrón significativamente más agradable para la expresividad del control de flujo secuencial, con generadores.

Terminología: Resolver, Cumplir y Rechazar

Hay una ligera confusión en torno a los términos «resolver», «cumplir» y «rechazar» que necesitamos aclarar, antes de que te adentres demasiado en el aprendizaje de las Promesas. Primero consideremos el constructor de Promise(...):

Como puede ver, se proporcionan dos callbacks (aquí llamadas X e Y). La primera se usa generalmente para marcar la Promesa como cumplida, y la segunda siempre marca la Promesa como rechazada. Pero, ¿de qué se trata el «normalmente», y qué implica eso de nombrar con precisión esos parámetros?

En última instancia, es sólo su código de usuario y los nombres de los identificadores no son interpretados por el motor con significado alguno, por lo que técnicamente no importa; foo(...) y bar(...) son igualmente funcionales. Pero las palabras que usas pueden afectar no sólo la forma en que piensas sobre el código, sino también la forma en que otros desarrolladores de tu equipo pensarán sobre ello. Pensar erróneamente en un código asíncrono cuidadosamente orquestado es casi seguro que va a ser peor que las alternativas del spaghetti-callback.

Así que realmente importa cómo los llames.

El segundo parámetro es fácil de decidir. Casi toda la literatura usa reject(...) como nombre, y como eso es exactamente (¡y sólo!) lo que hace, es una muy buena elección para el nombre. Te recomiendo encarecidamente que siempre uses reject(...).

Pero hay un poco más de ambigüedad en torno al primer parámetro, que en la literatura de Promise a menudo se denomina resolve(...). Esa palabra está obviamente relacionada con «resolución», que es lo que se usa en toda la literatura (incluyendo este libro) para describir el establecimiento de un valor/estado final a una Promesa. Ya hemos usado «resolver la Promesa» varias veces para que signifique o bien cumplir o bien rechazar la Promesa.

Pero si este parámetro parece ser usado para cumplir específicamente la Promesa, ¿por qué no deberíamos llamarlo fullfill(...) en lugar de resolve(...) para ser más precisos? Para responder a esta pregunta, echemos un vistazo a dos de los métodos de la API de Promise:

Promise.resolve(...) crea una Promesa que se resuelve con el valor que se le da. En este ejemplo, 42 es un valor normal, sin promesa, sin posibilidad de cambio, por lo que la promesa fulfilledPr se crea para el valor 42. Promise.reject("Oops") crea la promesa rechazada rejectedPr por el motivo «Oops».

Ahora vamos a ilustrar por qué la palabra «resolve» (como en Promise.resolve(...)) es inequívoca y de hecho más precisa, si se usa explícitamente en un contexto que podría resultar en cumplimiento o rechazo:

Como hemos discutido anteriormente en este capítulo, Promise.resolve(...) devolverá una Promesa genuina recibida directamente, o desenvolverá una petición recibida. Si ese desenvolvimiento revelara un estado rechazado, la Promesa devuelta de Promise.resolve(...) se encuentra de hecho en ese mismo estado rechazado.

Así que Promise.resolve(...) es un nombre bueno y preciso para el método de la API, porque puede resultar en cumplimiento o rechazo.

El primer parámetro del callback del constructor de Promise(...) desenvolverá un «thenable» (de la misma forma que Promise.resolve(…)) o una promesa genuina:

Debe quedar claro ahora que resolve(...) es el nombre apropiado para el primer parámetro de devolución de llamada del constructor de Promise(...).

Advertencia: El reject(...) mencionado anteriormente no desenvuelve como lo hace resolve(...). Si usted pasa un valor Promise/thenable reject(...), ese valor intacto se establecerá como la razón de rechazo. Un controlador de rechazo subsiguiente recibiría la Promesa/thenable actual que usted pasó a reject(...), no su valor inmediato subyacente.

Pero ahora volvamos nuestra atención a los callbacks proporcionados para then(...). ¿Cómo deberían llamarse (tanto en la literatura como en el código)? Yo sugeriría fulfilled(...) y rejected(...):

En el caso del primer parámetro de then(...), es inequívocamente siempre el caso de cumplimiento, por lo que no hay necesidad de la dualidad de la terminología «resolve». Como nota al margen, la especificación ES6 utiliza onFulfilled(...) y onRejected(...) para etiquetar estos dos callbacks, por lo que son términos precisos.

Tratamiento de errores


Ya hemos visto varios ejemplos de cómo el rechazo de Promise, ya sea intencional llamando a reject(...) o accidental a través de excepciones JS, permite un manejo más sensato de los errores en la programación asíncrona. Volvamos atrás y seamos más explícitos sobre algunos de los detalles que hemos pasado por alto.

La forma más natural de manejo de errores para la mayoría de los desarrolladores es la construcción síncrona try...catch. Desafortunadamente, sólo es síncrono, por lo que no ayuda en los patrones de código asíncronos:

Sería bonito disponer de try...catch, pero no funciona a través de operaciones asíncronas. A menos que haya algún apoyo en el entorno adicional, sobre el cual volveremos con los generadores en el Capítulo 4.

En los callbacks, han surgido algunos estándares para el manejo de errores con patrones, entre los que destaca el estilo de «el callback del error primero»(«error-first callback»):

Nota: El try...catch aquí funciona sólo desde la perspectiva de que la llamada baz.bar() tendrá éxito o fallará inmediatamente, sincrónicamente. Si baz.bar() fuera en sí mismo su propia función de completado asíncrona, cualquier error asíncrono en su interior no sería detectable.

El callback que pasamos a foo(...) espera recibir una señal de error por el primer parámetro err reservado. Si está presente, se supone un error. Si no es así, se asume el éxito.

Este tipo de manejo de errores es técnicamente capaz de ser asíncrono, pero no funciona bien en absoluto. Múltiples niveles de callbacks de error, junto con essas ubícuas sentencias if, inevitablemente le llevarán a los peligros del infierno del callback (ver Capítulo 2).

Así que volvamos al manejo de errores en Promises, con el manejador de rechazo pasado a then(...). Las promesas no utilizan el popular estilo de diseño del «callback del error primero», sino que utilizan el estilo de «callbacks divididos»; hay un callback para el cumplimiento y otro para el rechazo:

Mientras que este patrón de manejo de errores tiene sentido en la superficie, los matices del manejo de errores de Promise son a menudo un poco más difíciles de entender.

Considere:

Si el msg.toLowerCase() legítimamente arroja un error (¡lo hace!), ¿por qué no se notifica a nuestro gestor de errores? Como explicamos anteriormente, es porque ese manejador de errores es para la promesa p, que ya ha sido cumplida con el valor 42. La promesa p es inmutable, por lo que la única promesa que se puede notificar del error es la devuelta por p.then(...), que en este caso no capturamos.

Esto debería crear una imagen clara de por qué el manejo de errores con Promises es propenso a los errores (juego de palabras). Es demasiado fácil que se traguen los errores, ya que esto es muy raramente lo que se pretende.

Advertencia: Si utiliza la API de Promise de forma no válida y se produce un error que impide la correcta construcción de Promise, el resultado será una excepción inmediata, no una Promise rechazada. Algunos ejemplos de uso incorrecto que fallan en la construcción de la Promesa: new Promise(null), Promise.all(), Promise.race(42), y así sucesivamente. No puedes obtener una Promesa rechazada si no usas la API de Promesa de manera lo suficientemente válida como para construir una Promesa en primer lugar!

Foso de la Desesperación

Jeff Atwood señaló hace años: los lenguajes de programación a menudo se configuran de tal manera que, por defecto, los desarrolladores caen en el «pozo de la desesperación» (http://blog.codinghorror.com/falling-into-the-pit-of-success/), donde se castigan los accidentes, y que hay que esforzarse más por hacerlo bien. Nos imploró que en su lugar creáramos un «pozo de éxito», donde por defecto caes en la acción esperada (exitosa), y por lo tanto tendrías que esforzarte por fracasar.

El manejo de errores de las promesas es, sin duda, un diseño de «pozo de la desesperación». Por defecto, asume que usted quiere que cualquier error sea tragado por el estado de la Promesa, y si se olvida de observar ese estado, el error silenciosamente languidece/muere en la oscuridad – usualmente con desesperación.

Para evitar perder un error en el silencio de una Promesa olvidada/descartada, algunos desarrolladores han afirmado que una «mejor práctica» para las cadenas de Promesa es siempre terminar su cadena con un catch(...) final, como:

Debido a que no pasamos un manejador de rechazo al then(...), el manejador por defecto fue sustituido, lo que simplemente propaga el error a la siguiente promesa en la cadena. Como tal, tanto los errores que entran en p, como los errores que vienen después de p en su resolución (como el msg.toLowerCase()) se filtrarán hasta el handleErrors(...) final.

Problema resuelto, ¿verdad? ¡No tan rápido!

¿Qué pasa si handleErrors(...) también tiene un error? ¿Quién se da cuenta de eso? Todavía hay otra promesa desatendida: la que devuelve catch(...), que no capturamos y para la que no registramos un manejador de rechazos.

No se puede pegar otro catch(...) en el extremo de la cadena, porque también podría fallar. El último paso en cualquier cadena de la Promesa, sea lo que sea, siempre tiene la posibilidad, aunque sea cada vez menos, de colgarse de un error no detectado atrapado dentro de una Promesa no cumplida.

¿Sigue pareciebdo un acertijo imposible?

Manipulación no capturada

No es exactamente un problema fácil de resolver completamente. Hay otras maneras de abordarlo que muchos dirían que son mejores.

Algunas librerías Promise han añadido métodos para registrar algo como un manejador de «rechazo global desatendido», que se llamaría en lugar de un error lanzado globalmente. Pero su solución para identificar un error como «no detectado» es tener un temporizador de duración arbitraria, digamos de 3 segundos, desde el momento del rechazo. Si se rechaza una Promise pero no se registra ningún gestor de errores antes de que se dispare el temporizador, se supone que nunca se registrará un gestor, por lo que es «no capturado».

En la práctica, esto ha funcionado bien para muchas bibliotecas, ya que la mayoría de los patrones de uso no requieren un retraso significativo entre el rechazo de Promise y la observación de ese rechazo. Pero este patrón es problemático porque 3 segundos es tan arbitrario (incluso empírico), y también porque hay casos en los que quieres que una Promesa se aferre a su rechazo por un tiempo indefinido, y no quieres que tu manejador «no capturado» llame a todos esos falsos positivos («errores no capturados» aún no manejados).

Otra sugerencia más común es que a las Promesas se les debe añadir un done(...), lo que esencialmente marca la cadena de Promesas como «hecho.» done(...) no crea y devuelve una Promesa, por lo que las llamadas de retorno pasadas a done(...) obviamente no están conectadas para reportar problemas a una Promesa encadenada que no existe.

Entonces, ¿qué pasa en su lugar? Se trata como es de esperar en condiciones de error no capturado: cualquier excepción dentro de un manejador de rechazos de done(...) sería lanzada como un error global no capturado (en la consola del desarrollador, básicamente):

Esto puede sonar más atractivo que la cadena interminable o los tiempos de espera arbitrarios. Pero el mayor problema es que no forma parte del estándar ES6, así que no importa lo bien que suene, en el mejor de los casos está muy lejos de ser una solución fiable y ubicua.

¿Estamos atascados, entonces? No del todo.

Los navegadores tienen una capacidad única que nuestro código no tiene: pueden rastrear y saber con seguridad cuándo se deshecha cualquier objeto y se recoge la basura. Por lo tanto, los navegadores pueden rastrear los objetos Promise, y cada vez que recogen basura, si tienen un rechazo en ellos, el navegador sabe con seguridad que se trata de un «error no detectado» legítimo y, por lo tanto, puede saber con seguridad que debe informar de ello a la consola del desarrollador.

Nota: En el momento de escribir este artículo, tanto Chrome como Firefox tienen intentos tempranos de obtener ese tipo de capacidad de «rechazo no descubierto», aunque el soporte es incompleto en el mejor de los casos.

Sin embargo, si una Promesa no hace que se recoja basura — es extremadamente fácil que eso suceda accidentalmente a través de muchos patrones de codificación diferentes — la recolección de basura del navegador no le ayudará a saber y diagnosticar que usted tiene una Promesa rechazada silenciosamente por ahí.

¿Hay alguna otra alternativa? Sí.

El pozo del éxito

Lo siguiente es sólo teórico, el cómo las Promesas podrían ser cambiadas algún día para que se comporten. Creo que sería muy superior a lo que tenemos actualmente. Y creo que este cambio sería posible incluso después de ES6 porque no creo que rompa la compatibilidad web con ES6 Promises. Además, se puede rellenar con polyfils, si se tiene cuidado. Echemos un vistazo:

  • Las promesas podrían no informar (a la consola del desarrollador) de cualquier rechazo, en el siguiente tictac del bucle de trabajo o evento, si en ese momento exacto no se ha registrado ningún gestor de errores para la promesa.
  • Para los casos en los que desee que una Promesa rechazada se mantenga en su estado rechazado durante un tiempo indefinido antes de la observación, puede llamar a defer(), que suprime el informe automático de errores de esa Promesa.

Si se rechaza una Promesa, por defecto informa ruidosamente de ese hecho a la consola del desarrollador (en lugar de silenciarla por defecto). Puede optar por excluir ese informe de forma implícita (registrando un gestor de errores antes del rechazo) o explícita (con defer()). En cualquier caso, usted controla los falsos positivos.

Considere:

Cuando creamos p, sabemos que vamos a esperar un rato para usar/observar su rechazo, así que llamamos defer() — así que no hay informes globales. defer() simplemente devuelve la misma promesa, para propósitos de encadenamiento.

La promesa devuelta por foo(...) obtiene un gestor de errores de inmediato, por lo que se ha optado implícitamente por no hacerlo y tampoco se produce ningún informe global.

Pero la promesa devuelta de la llamada then(...) no tiene ningún defer() o manejador de errores adjunto, así que si rechaza (desde dentro de cualquiera de los manejadores de resolución), entonces será reportada a la consola del desarrollador como un error no detectado.

Este diseño es un pozo de éxito. Por defecto, todos los errores son manejados o reportados — lo que casi todos los desarrolladores en casi todos los casos esperarían. Usted tiene que registrar a un manejador o tiene que optar intencionalmente por excluirse, e indicar que tiene la intención de diferir el manejo de errores hasta más adelante; usted está optando por la responsabilidad adicional en ese caso específico.

El único peligro real en este enfoque es si defer() (difiere) una Promesa pero luego realmente no se observa/maneja su rechazo.

Pero tuviste que llamar intencionadamente a defer() para optar por ese pozo de desesperación — el pozo del éxito — así que no hay mucho más que podamos hacer para salvarte de tus propios errores.

Creo que todavía hay esperanza para el manejo de errores de Promise (post-ES6). Espero que las autoridades reconsideren la situación y consideren esta alternativa. Mientras tanto, puede implementarlo usted mismo (¡un ejercicio desafiante para el lector!), o utilizar una biblioteca Promise más inteligente que lo haga por usted.

Nota: Este modelo exacto para el manejo de errores e informes está implementado en mi biblioteca de abstracción de Promesa asíncrona, que será discutido en el Apéndice A de este libro.

Patrones de Promesa


Ya hemos visto implícitamente el patrón de secuencia con las cadenas de Promesa (el control de flujo esto-entonces-esto-entonces-eso) pero hay muchas variaciones en los patrones asíncronos que podemos construir como abstracciones encima de las Promesas. Estos patrones sirven para simplificar la expresión del control de flujo asíncrono, lo que ayuda a que nuestro código sea más razonable y más mantenible, incluso en las partes más complejas de nuestros programas.

Dos de estos patrones están codificados directamente en la implementación nativa de la Promesa ES6, así que los obtenemos gratuitamente, para usarlos como bloques de construcción para otros patrones.

Promeise.all([… ])

En una secuencia asíncrona (la cadena de promesa), sólo se coordina una tarea asíncrona en un momento dado: el paso 2 sigue estrictamente al paso 1, y el paso 3 sigue estrictamente al paso 2. Pero, ¿qué hay de hacer dos o más pasos simultáneamente (también conocido como «en paralelo»)?

En la terminología clásica de programación, una «puerta» es un mecanismo que espera a que dos o más tareas paralelas/concurrentes se completen antes de continuar. No importa en qué orden terminen, sólo que todos ellos tienen que completarse para que la puerta se abra y deje pasar el control del flujo.

En la API de Promise, llamamos a este patrón all([... ]).

Supongamos que quiere hacer dos solicitudes de Ajax al mismo tiempo, y esperar a que ambos terminen, independientemente de su orden, antes de hacer una tercera solicitud de Ajax. Considere:

Promise.all([... ]) espera un único argumento, una matriz, que consiste generalmente en instancias de Promise. La promesa devuelta de la llamada Promise.all([... ]) recibirá un mensaje de cumplimiento (msgs en este fragmento) que es una matriz de todos los mensajes de cumplimiento de las promesas pasadas, en el mismo orden que se especifica (independientemente de la orden de cumplimiento).

Nota: Técnicamente, el conjunto de valores pasados a Promise.all([... ]) puede incluir Promesas, thenables, o incluso valores inmediatos. Cada valor en la lista es esencialmente pasado a través de Promise.resolve(...) para asegurarse de que es una Promesa genuina por la que hay que esperar, así que un valor inmediato se normalizará en una Promesa por ese valor. Si el array está vacío, la Promesa principal se cumple inmediatamente.

La promesa principal devuelta de Promise.all([... ]) sólo se cumplirá cuando se cumplan todas las promesas que la componen. Si alguna de esas promesas es rechazada, la promesa principal Promise.all([... ]) es rechazada inmediatamente, descartando todos los resultados de cualquier otra promesa.

Recuerda siempre asignar un manejador de rechazo/error a cada promesa, incluso y especialmente a la que regresa de Promise.all([... ]).

Promise.race([ .. ])

Mientras que Promise.all([... ]) coordina múltiples Promesas simultáneamente y asume que todas son necesarias para su cumplimiento, a veces sólo se quiere responder a la «primera Promesa para cruzar la línea de meta», dejando que las otras Promesas se caigan.

Este patrón se llama clásicamente «cerrojo», pero en Promises se llama «carrera».

Advertencia: Mientras que la metáfora de «sólo el primero que cruza la línea de meta gana» se ajusta bien al comportamiento, desafortunadamente «carrera» es una especie de término cargado, porque las «condiciones de carrera» son generalmente tomadas como errores en los programas (ver Capítulo 1). No confundir Promise.race([... ]) con «condición de carrera».

Promise.race([... ]) también espera un único argumento de array, que contenga una o más Promesas, thenables o valores inmediatos. No tiene mucho sentido práctico tener una carrera con valores inmediatos, porque el primero en la lista obviamente ganará – ¡como una carrera donde un corredor comienza en la línea de meta!

Similar a Promise.all([... ]), Promise.race([... ]) cumplirá sólo cuando cualquier resolución de Promise sea cumplida, y rechazará cuando cualquier resolución de Promise es rechazada.

Advertencia: Una «carrera» requiere al menos un «corredor», por lo que si pasas una matriz vacía, en lugar de resolverla inmediatamente, la promesa principal race([... ]) nunca se resolverá. ¡Esto es un arma de fuego! ES6 debería haber especificado que cumple, rechaza o simplemente arroja algún tipo de error síncrono. Desafortunadamente, debido a la precedencia de las librerías Promise anteriores a ES6 Promise, tuvieron que dejar este error ahí dentro, así que ten cuidado de no enviar nunca una matriz vacía.

Volvamos a nuestro ejemplo anterior de Ajax concurrente, pero en el contexto de una carrera entre p1 y p2:

Como sólo gana una promesa, el valor de cumplimiento es un único mensaje, no una matriz como lo fue para Promise.all ([... ]).

Carrera de tiempo muerto

Hemos visto este ejemplo anteriormente, ilustrando cómo Promise.race([... ]) puede ser usado para expresar el patrón «tiempo de espera de la promesa»:

Este patrón de tiempo de espera funciona bien en la mayoría de los casos. Pero hay algunos matices a considerar, y francamente se aplican tanto a Promise.race([... ]) como a Promise.all([... ]) por igual.

«Por fin»

La pregunta clave es: «¿Qué pasa con las promesas que se descartan/ignoran?» No estamos haciendo esa pregunta desde la perspectiva del rendimiento — típicamente terminarían siendo elegibles para la recolección de basura — sino desde la perspectiva del comportamiento (efectos secundarios, etc.). Las promesas no pueden ser canceladas – y no deberían poder serlo, ya que eso destruiría la confianza en la inmutabilidad externa discutida en la sección «Promesa Incancelable» más adelante en este capítulo – por lo que sólo pueden ser ignoradas en silencio.

Pero, ¿y si foo() en el ejemplo anterior está reservando algún tipo de recurso para su uso, pero el tiempo de espera se dispara primero y hace que esa promesa sea ignorada? ¿Hay algo en este patrón que libere proactivamente el recurso reservado después del tiempo de espera, o que cancele cualquier efecto secundario que pueda haber tenido? ¿Y si todo lo que querías era registrar el hecho de que foo() agotó el tiempo?

Algunos desarrolladores han propuesto que Promises necesita un registro de devolución de llamada finaly(...), que siempre se llama cuando se resuelve una promesa, y le permite especificar cualquier limpieza que pueda ser necesaria. Esto no existe en la especificación en este momento, pero puede venir en ES7+. Tendremos que esperar y ver.

Podría parecer que sí:

Nota: En varias bibliotecas de Promise, finally(...) todavía crea y devuelve una nueva Promise (para mantener la cadena en marcha). Si la función cleanup(...) fuera a devolver una Promesa, estaría enlazada a la cadena, lo que significa que aún podría tener los problemas de rechazo que discutimos anteriormente.

Mientras tanto, podríamos hacer una utilidad de ayuda estática que nos permita observar (sin interferir) la resolución de una Promesa:

Así es como lo usaríamos en el ejemplo del timeout de antes:

Este ayudante de Promise.observe(...) es sólo una ilustración de cómo se pueden observar los cumplimientos de las Promesas sin interferir con ellas. Otras bibliotecas de Promise tienen sus propias soluciones. Independientemente de cómo lo hagas, es probable que tengas lugares donde quieras asegurarte de que tus Promesas no sean ignoradas silenciosamente por accidente.

Variaciones sobre all([... ]) y race([... ])

Mientras que las Promesas ES6 nativas vienen con Promise.all([... ]) y Promise.race([... ])incorporadas, hay otros patrones comúnmente usados con variaciones en esa semántica:

  • none([... ]) es como all([... ]), pero los cumplimientos y los rechazos se transponen. Todas las promesas deben ser rechazadas – los rechazos se convierten en valores de cumplimiento y viceversa.
  • any([... ]) es como all([... ]), pero ignora cualquier rechazo, por lo que sólo uno necesita cumplir en lugar de todos ellos.
  • first([... ]) es como una carrera con any([... ]), que es que ignora cualquier rechazo y cumple tan pronto como la primera Promesa cumple.
  • last([... ]) es como first([... ]), pero sólo gana el último cumplimiento.

Algunas librerías de abstracción de Promise las proporcionan, pero también puedes definirlas tú mismo usando la mecánica de Promises, race([... ]) y all([... ]).

Por ejemplo, así es como podríamos definir first([... ]):

Nota: Esta implementación de first(...) no rechaza si todas sus promesas se rechazan; simplemente cuelga, como lo hace una Promise.race([]). Si lo desea, puede agregar lógica adicional para rastrear cada rechazo de promesa y si todos rechazan, llame a reject() en la promesa principal. Dejaremos eso como un ejercicio para el lector.

Iteraciones concurrentes

A veces se quiere iterar sobre una lista de Promesas y realizar alguna tarea contra todas ellas, de la misma manera que se puede hacer con matrices síncronas (por ejemplo, forEach(...), map(...), some(...), y every(...)). Si la tarea a realizar contra cada Promesa es fundamentalmente síncrona, estas funcionan bien, tal como usamos forEach(...) en el fragmento anterior.

Pero si las tareas son fundamentalmente asíncronas, o pueden/deberían realizarse simultáneamente, puede utilizar versiones asíncronas de estas utilidades, tal y como proporcionan muchas bibliotecas.

Por ejemplo, consideremos una utilidad asíncrona map(...) que toma una serie de valores (podrían ser Promesas o cualquier otra cosa), más una función (tarea) a realizar contra cada uno. map(...) en sí misma devuelve una promesa cuyo valor de cumplimiento es una serie que contiene (en el mismo orden de mapeo) el valor de cumplimiento asíncrono de cada tarea:

Nota: En esta implementación de map(...), no se puede señalar el rechazo asíncrono, pero si ocurre una excepción/error síncrono dentro del callback del mapeo (cb(...)), la promesa principal devuelta por Promise.map(..) sería rechazada.

Vamos a ilustrar el uso de map(...) con una lista de Promesas (en lugar de valores simples):

Resumen de la API de Promise


Revisemos la API de ES6 Promise que ya hemos visto desplegarse en pedacitos a lo largo de este capítulo.

Nota: La siguiente API es nativa sólo a partir de ES6, pero hay polifylls que cumplen con las especificaciones (no sólo bibliotecas Promise extendidas) que pueden definir Promise y todo su comportamiento asociado para que pueda utilizar Promises nativas incluso en navegadores anteriores a ES6. Uno de estos polyfill es «Native Promise Only» (http://github.com/getify/native-promise-only), que yo escribí!

Constructor new Promise(...)

El constructor que crea Promise(...) debe ser usado con new, y debe ser provisto de una función callback que sea llamada síncronamente/de inmediato. A esta función se le pasan dos funciones callbacks que actúan como capacidades de resolución para la promesa. Comúnmente etiquetamos estas como resolve(...) y reject(...):

reject(...) simplemente rechaza la promesa, pero resolve(...) puede cumplir la promesa o rechazarla, dependiendo de lo que se le haya haya pasado. Si a resolve(...) se le pasa un valor inmediato, sin promesa, un no thenable, entonces la promesa se cumple con ese valor.

Pero si a resolve(...) se le pasa una Promesa genuina o un valor thenable, ese valor se desenvuelve recursivamente, y cualquiera que sea su resolución/estado final será adoptado por la promesa.

Promise.resolve(...) y Promise.reject(...)

Un atajo para crear una promesa ya rechazada es Promise.reject(...), por lo que estas dos promesas son equivalentes:

Promise.resolve(...) se utiliza normalmente para crear una Promesa ya cumplida de forma similar a Promise.reject(...). Sin embargo, Promise.resolve(...) también desenvuelve los valores thenebles (como ya se ha comentado varias veces). En ese caso, la Promesa devuelta adopta la resolución final del thenable pendiente, que puede ser el cumplimiento o el rechazo:

Y recuerda, Promise.resolve(...) no hace nada si lo que pasas ya es una Promesa genuina; sólo devuelve el valor directamente. Así que no hay gastos generales en llamar a Promise.resolve(...) sobre valores de los que no se conoce la naturaleza, si es que uno ya es una Promesa genuina.

then(..) y catch(..)

Cada instancia de Promise (no el espacio de nombres de la API de Promise) tiene métodos then(...) y catch(...), que permiten registrar a los manejadores de cumplimiento y rechazo para la Promesa. Una vez resuelta la Promesa, se llamará a uno u otro de estos manejadores, pero no a ambos, y siempre se llamará asíncronamente (ver «Trabajos» en el Capítulo 1).

then(...) toma uno o dos parámetros, el primero para la llamada de retorno de cumplimiento, y el segundo para la llamada de retorno de rechazo. Si se omite o se pasa como un valor no funcional, se sustituye una llamada de retorno por defecto respectivamente. La llamada de retorno de cumplimiento por defecto simplemente pasa el mensaje, mientras que la llamada de retorno de rechazo por defecto simplemente repite (propaga) la razón del error que recibe.

catch(...) toma sólo la llamada de rechazo como parámetro, y automáticamente sustituye la llamada de cumplimiento por defecto, como se acaba de discutir. En otras palabras, es equivalente a then(null,...):

then(...) y catch(...) también crean y devuelven una nueva promesa, que puede ser utilizada para expresar el control del flujo de la cadena Promesa. Si el callback de cumplimiento o rechazo tiene una excepción, la promesa devuelta es rechazada. Si cualquiera de las dos devuelve un valor inmediato, no una promesa, no un thenable, ese valor se establece como el cumplimiento de la promesa devuelta. Si el gestor de cumplimiento devuelve específicamente una promesa o valor thenable, ese valor se desenvuelve y se convierte en la resolución de la promesa devuelta.

Promise.all([... ]) y Promise.race([... ])

Los ayudantes estáticos Promise.all([... ]) y Promise.race([... ]) en la API de ES6 Promise crean una Promesa como su valor de retorno. La resolución de esa promesa está totalmente controlada por el array de promesas que le pasas.

Para Promise.all([... ]), todas las promesas que pases deben ser cumplidas para que la promesa devuelta sea cumplida. Si alguna promesa es rechazada, la principal promesa devuelta es rechazada inmediatamente, también (descartando los resultados de cualquiera de las otras promesas). Para el cumplimiento, recibes un conjunto de todos los valores de cumplimiento de las promesas. Para el rechazo, recibes sólo la primera promesa de valor de razón de rechazo. Este patrón se llama clásicamente una «puerta»: todos deben llegar antes de que la puerta se abra.

Para Promise.race([... ]), sólo la primera promesa a resolver (cumplimiento o rechazo) «gana», y cualquiera que sea esa resolución se convierte en la resolución de la promesa devuelta. Este patrón se llama clásicamente un «cerrojo»: el primero que abra el cerrojo pasa. Considere: