Las promesas son un mecanismo central para manejar código asincrónico en JavaScript. Los encontrará en muchas bibliotecas y marcos de JavaScript, donde se utilizan para gestionar los resultados de una acción. El fetch()
API es un ejemplo de promesas en funcionamiento. Como desarrollador, es posible que no esté familiarizado con la creación y el uso de promesas fuera de un producto existente, pero es sorprendentemente simple. Aprender a crear promesas le ayudará a comprender cómo las utilizan las bibliotecas. También pone a tu disposición un potente mecanismo de programación asincrónica.
Programación asincrónica con promesas.
En el siguiente ejemplo, estamos usando un Promise
para manejar los resultados de una operación de red. En lugar de realizar una llamada de red, simplemente utilizamos un tiempo de espera:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = "This is the fetched data!";
resolve(data);
}, 2000);
});
}
const promise = fetchData();
promise.then((data) => {
console.log("This will print second:", data);
});
console.log("This will print first.");
En este código definimos un fetchData()
función que devuelve un Promise
. Llamamos al método y mantenemos el Promise
en el promise
variable. Luego usamos el Promise.then()
método para abordar los resultados.
La esencia de este ejemplo es que el fetchData()
La llamada ocurre inmediatamente en el flujo de código, mientras que la devolución de llamada pasa a then()
solo ocurre después de que se completa la operación asincrónica.
Si miras dentro fetchData()
verás que define un Promise
objeto, y ese objeto toma una función con dos argumentos: resolve
y reject
. Si el Promise
tiene éxito, llama resolve
; si hay un problema llama reject
. En nuestro caso, simulamos el resultado de una llamada de red llamando resolve
y devolver una cadena.
A menudo verás el Promise
llamado y manejado directamente, así:
fetchData().then((data) => {
console.log("This will print second:", data);
});
Ahora pensemos en los errores. En nuestro ejemplo, podemos simular una condición de error:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.5) {
reject("An error occurred while fetching data!");
} else {
const data = "This is the fetched data!";
resolve(data);
}
}, 2000);
});
}
Aproximadamente la mitad de las veces, la promesa en este código fallará al llamar reject()
. En una aplicación del mundo real, esto podría suceder si la llamada de red falla o el servidor devuelve un error. Para manejar la posibilidad de falla al llamar fetchData()
usamos catch()
:
fetchData().then((data) => {
console.log("That was a good one:", data);
}).catch((error) => {
console.log("That was an error:", error)
});
Si ejecuta este código varias veces, obtendrá una combinación de errores y éxitos. Considerándolo todo, es una forma sencilla de describir su comportamiento asincrónico y luego consumirlo.
Cadenas de promesa en JavaScript
Una de las bellezas de las promesas es que puedes encadenarlas. Esto ayuda a evitar devoluciones de llamadas profundamente anidadas y simplifica el manejo de errores asincrónicos anidados. (No voy a enturbiar las aguas mostrando una antigua función de JavaScript con devolución de llamada de argumento. Créame, eso se vuelve complicado).
Dejando nuestro fetchData()
funciona como está, agreguemos un processData()
función. El processData()
función depende de los resultados de fetchData()
. Ahora, podríamos envolver la lógica de procesamiento dentro de la llamada de devolución desde fetchData()
pero las promesas nos permiten hacer algo mucho más limpio:
function processData(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const processedData = data + " - Processed";
resolve(processedData);
}, 1000);
});
}
fetchData()
.then((data) => {
console.log("Fetched data:", data);
return processData(data);
})
.then((processedData) => {
console.log("Processed data:", processedData);
})
.catch((error) => {
console.error("Error:", error);
});
Si ejecuta este código varias veces, notará que cuando fetchData()
tiene éxito, ambos then()
Los métodos se llaman correctamente. Cuando fetchData()
falla, toda la cadena se cortocircuita y el final catch()
son llamados. Esto es similar a cómo funcionan los bloques try/catch.
Si tuviéramos que poner el catch()
después del primero then()
sería responsable sólo de fetchData()
errores. En este caso, nuestro catch()
se encargará tanto de fetchData()
y processData()
errores.
La clave aquí es que fetchData()
‘s then()
manejador devuelve la promesa de processData(data)
. Eso es lo que nos permite encadenarlos.
Ejecutar pase lo que pase: Promise.finally()
Al igual que try/catch te da una finally()
, Promise.finally()
se ejecutará sin importar lo que suceda en la cadena de promesa:
fetchData()
.then((data) => {
console.log("Fetched data:", data);
return processData(data);
})
.then((processedData) => {
console.log("Processed data:", processedData);
})
.catch((error) => {
console.error("Error:", error);
})
.finally(() => {
console.log("Cleaning up.");
})
El finally()
Es útil cuando necesitas hacer algo pase lo que pase, como cerrar una conexión.
Falla rápido: Promise.all()
Ahora consideremos una situación en la que necesitamos realizar varias llamadas simultáneamente. Digamos que necesitamos realizar dos solicitudes de red y necesitamos los resultados de ambas. Si cualquiera de los dos falla, queremos que toda la operación falle. Nuestro enfoque de encadenamiento anterior podría funcionar, pero no es ideal porque requiere que finalice una solicitud antes de que comience la siguiente. En lugar de eso, podemos usar Promise.all()
:
Promise.all([fetchData(), fetchOtherData()])
.then((data) => { // data is an array
console.log("Fetched all data:", data);
})
.catch((error) => {
console.error("An error occurred with Promise.all:", error);
});
Debido a que JavaScript es de un solo subproceso, estas operaciones no son realmente concurrentes, pero están mucho más cerca. En particular, el motor JavaScript puede iniciar una solicitud y luego iniciar la otra mientras aún está en proceso. Este enfoque nos acerca lo más posible a la ejecución paralela que podemos conseguir con JavaScript.
Si alguna de las promesas pasara a Promise.all()
falla, detendrá toda la ejecución e irá a la página proporcionada catch()
. De ese modo, Promise.all()
es «fallar rápido».
También puedes usar finally()
con Promise.all()
y se comportará como se esperaba, ejecutándose sin importar cómo resulte el conjunto de promesas.
En el then()
método, recibirá una matriz, con cada elemento correspondiente a la promesa pasada, así:
Promise.all([fetchData(), fetchData2()])
.then((data) => {
console.log("FetchData() = " + data[0] + " fetchMoreData() = " + data[1] );
})
Que gane el más rápido: Promise.race()
A veces tienes varias tareas asincrónicas pero sólo necesitas la primera para tener éxito. Esto podría suceder cuando tienes dos servicios redundantes y quieres utilizar el más rápido.
Digamos fetchData()
y fetchSameData()
Hay dos formas de solicitar la misma información y ambas devuelven promesas. Así es como usamos race()
para gestionarlos:
Promise.race([fetchData(), fetchSameData()])
.then((data) => {
console.log("First data received:", data);
});
En este caso, el then()
La devolución de llamada recibirá solo un valor para los datos: el valor de retorno del ganador (el más rápido) Promise
.
Los errores están ligeramente matizados con race()
. si el rechazado Promise
es lo primero que sucede, luego toda la carrera termina y catch()
se llama. Si la promesa rechazada ocurre después de que se haya resuelto otra promesa, el error se ignora.
Todo o ninguno: Promise.allSettled()
Si desea esperar a que se complete una colección de operaciones asíncronas, ya sea que fallen o tengan éxito, puede usar allSettled()
. Por ejemplo:
Promise.allSettled([fetchData(), fetchMoreData()]).then((results) =>
results.forEach((result) => console.log(result.status)),
);
El results
argumento pasado al then()
El controlador contendrá una matriz que describe los resultados de las operaciones, algo como:
[0: {status: 'fulfilled', value: "This is the fetched data!"},
1: {status: 'rejected', reason: undefined}]
Entonces obtienes un campo de estado que es fulfilled
o rejected
. Si se cumple (se resuelve), entonces el valor contendrá el argumento llamado por resolve()
. Las promesas rechazadas poblarán el reason
campo con la causa del error, suponiendo que se haya proporcionado una.
Próximamente: Promise.withResolvers()
El Especificaciones ECMAScript 2024 incluye un método estático en Promise
llamado withResolvers()
. La mayoría de los navegadores y entornos del lado del servidor ya lo admiten. Es un poco esotérico, pero Mozilla tiene una buena ejemplo de como se usa. El nuevo método le permite declarar un Promise
junto con resolve
y reject
funcionan como variables independientes manteniéndolas en el mismo ámbito.
Conclusión
Las promesas son un aspecto importante y útil de JavaScript. Pueden brindarle la herramienta adecuada en una variedad de situaciones de programación asincrónica y aparecen todo el tiempo cuando se utilizan marcos y bibliotecas de terceros. Los elementos cubiertos en este tutorial son todos componentes de alto nivel, por lo que es una API bastante sencilla de conocer.
Copyright © 2024 IDG Communications, Inc.