Promesas
¿Qué es una promesa?
Una promesa es un objeto de la clase Promise, que se crea para resolver una acción asíncrona, es decir, una operación que puede tardar un tiempo considerable y no queremos bloquear el hilo principal del programa mientras se procesa. El constructor recibe una función con dos parámetros, resolve y reject. Estos parámetros a su vez son funciones.
resolve se llama cuando la acción acaba correctamente devolviendo (opcionalmente) algún dato, mientras que reject se usa cuando se produce un error. Una promesa acaba cuando se llama a una de estas dos funciones.
Para procesar el resultado de una promesa, tenemos que suscribirnos a ella llamando al método then, pasándole una función que manejará el resultado. Esta función se ejecutará cuando se resuelva la promesa correctamente (sin errores).
La llamada al método then a su vez devuelve otra promesa. Es por ello que podremos concatenar varias llamadas a dicho método. El primero recibe el resultado de la promesa original, mientras que los restantes reciben como resultado lo que devuelva la función del método then anterior. Pueden devolver cualquier tipo de valor, incluyendo otra promesa. El siguiente then recibiría el resultado de dicha promesa una vez se resuelva.
Iremos directamente al bloque catch si lanzamos un error dentro de una sección then. O si la promesa original se rechaza con reject.
Opcionalmente, por legibilidad o reutilización de código, podemos separar las funciones del bloque then (o catch) en lugar de usar funciones anónimas o arrow functions.

Tanto el método then, como el método catch, pueden devolver un valor directamente u otra promesa. Esto significa que podemos usar el método catch para recuperarnos de un error, obteniendo datos de otro sitio alternativo, por ejemplo. Lo que devuelve un método catch se recogería en un método then que se concatene posteriormente.
Hay otro método llamado finally, que se puede añadir al final y se ejecuta siempre, haya error o no. Se puede usar por ejemplo para ocultar una animación de carga de datos.
Promise.resolve y Promise.reject
Imaginemos por ejemplo. que tenemos una función de la cual se espera que devuelva los datos dentro de una promesa, llamando a un servicio web por ejemplo. Pero puede ser que tengamos los datos ya cargados previamente y se pueda ahorrar dicha llamada.
Como lo esperable es que devuelva siempre una promesa, para matener la consistencia de tipos, puede devolver inmediatamente dicho valor que ya tenemos almacenado encapsulado en una promesa con Promise.resolve, o una promesa con error usando Promise.reject. De esta forma nos ahorramos crear la promesa manualmente llamando al constructor (solo en el caso de promesas que resuelvan inmediatamente).
Otros métodos de Promise
Promise.all
A veces tenemos que crear varias promesas en paralelo (varias llamadas a un servidor para obtener diferentes datos). Si necesitamos esperar para hacer algo una vez todas terminen en lugar de procesarlas por separado, podemos agruparlas en un array y utilizar el método Promise.all. Una vez haya terminado la última, se ejecutará el método then asociado que recibirá un array con los resultados de cada promesa.
Si alguna de estas promesas es rechazada (error), se ignorará inmediatamente a las promesas pendientes, ejecutando directamente el método catch (si lo hubiera).
Promise.allSettled
Parecido al método anterior. Si queremos esperar a que todas las promesas acaben, aunque una o varias devuelvan un error, tenemos el método Promise.allSettled. En este caso, el método then recibirá un array de objetos (1 por promesa). Cada objeto tendrá 3 posibles propiedades:
- status: Contendrá los valores "fulfilled" (todo ha ido bien), o "rejected" (error).
- value: Cuando la promesa tenga el estado "fulfilled", contendrá el valor devuelto.
- reason: Cuando la promesa tenga el estado "rejected", contendrá el error.
Promise.race
Otro método interesante es Promise.race. En este caso, devuelve el resultado de la primera promesa que termina, ignorando el resto. Útil, por ejemplo, si podemos obtener un dato de varias fuentes diferentes, y queremos la más rápida.
Async / Await
Las instrucciones async y await fueron introducidas en la versión ES2017. Se utilizan para que la sintaxis al trabajar con promesas sea más amigable.
await se utiliza para esperar que una promesa termine y nos devuelva su valor. El problema es que eso bloquea la ejecución del siguiente código hasta que la promesa acabe. Esto implicaría bloquear el programa durante un tiempo indeterminado. Por ello, sólo podemos usarlas dentro de funciones de tipo async. Estas funciones se ejecutan de forma asíncrona al resto del código, lo que impide que bloqueen el hilo principal.
Una función async siempre nos devuelve una promesa, que contendrá el valor devuelto por la instrucción return. Si no hay instrucción return, también devuelve una promesa, aunque vacía (Promise<void>).
Import dinámico con promesas
Además de la instrucción import, se puede importar un módulo con la función import(). Esta función devuelve una promesa que se resuelve una vez que el módulo ha sido descargado y procesado, y nos devuelve un objeto que contiene todo lo que el módulo exporta. Esto nos permite hacer imports dinámicos, donde el nombre del módulo puede estar en una variable o simplemente, no cargar un módulo hasta que realmente lo necesitemos por cuestiones de eficiencia y rendimiento.
Otro uso muy común es el de no cargar un módulo hasta que se necesite, de tal forma que la carga inicial del programa es más rápida si evitamos la carga de módulos (librerías) pesadas como las de tratamiento de imágenes, etc. hasta que no se necesiten.
En resumen: Mientras que la carga de módulos con import es estática, y debe procesar el módulo y cargarlo antes de continuar (si no se ha hecho previamente), la carga con la función import() es asíncrona y se puede hacer en cualquier momento del código.
top-level await
Desde ES2020 tenemos top-level await, lo que significa que no tenemos que crear funciones async para poder utilizar await en el código principal. Pero debemos recordar que que la ejecución del resto del código se bloqueará hasta que se resuelva la instrucción await. Esto solo debemos utilizarlo si el resto del código en el módulo actual es totalmente dependiente del resultado de la promesa (en este caso del módulo que cargamos). En caso contrario, o en caso de duda, no lo utilices.
Utilizarlo para cargar módulos de los cuales depende el resto del código del módulo actual, y que no podemos cargar con import (el nombre depende de una variable, o la carga de uno u otro módulo dependen de una condición), está justificado.