hand Inicio
hand JSBloqs
hand GutenBloqs

Combinando promesas (await async) con loops en JavaScript

Los loops forEach y map no funcionan como los clásicos for, con lo que su uso con await y async se complica

Promesas con await y async

Las promesas en JavaScript fueron un gran qué, pero eran y son un pequeño caos

El objetivo? Conseguir una manera de no bloquear la ejecución del código mientras una instrucción aún no se había completado

  • Por ejemplo, leer datos de una fuente externa

Cómo funcionan las promesas? Nos devuelven una promise y una manera de saber cuándo esa promesa se resuelve

El concepto es perfecto, pero en código la implementación no fue brillante

Entonces vino el async y el await, una manera de expresar promesas que sí es brillante

Por ejemplo, vamos a suponer que aquí la función fetchMyData nos devuelve una promesa

js
const fetching = () => {
const data = fetchMyData()
console.log(data) // me sale "pending"
}

fetching()

En este código necesitamos esperar a que data exista (es decir a que la promesa que nos devuelve fetchMyData se resuelva)

Pues tan sencillo como esto

js
const fetching = async () => {
const data = await fetchMyData()
console.log(data) // data es lo que sea que fuere data
}

fetching()

Y cuando estamos en un loop qué, funciona igual?

Para los loops "clásicos" la respuesta era sí, funcionan igual

Por ejemplo con un for..of

js
// función que implementa un wait con promesas
const wait = ms => new Promise((r, j) => setTimeout(r, ms))

// función que emula un proceso asíncrono
const fetchUrl = async url => {
await wait(1000)
return 'fantastic ' + url
}

// aquí vamos a hacer ver que pedimos 3 webs, y lo hacemos con un loop for .. of
const fetching = async () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']
for (const url of urls) {
const data = await fetchUrl(url)
console.log(data)
}
}

fetching()

En este loop las cosas funcionan como queremos, un console.log a cada segundo, fabuloso

Pero y si queremos un loop menos declarativo y más funcional?

Por loops funcionales tenemos el forEach y el map, y estos loops no funcionan como esperaríamos

La diferencia entre un forEach y un map es que el segundo nos devuelve un valor, y el primero no

Vamos a verlo con un forEach, y verás que he puesto el async dentro de la función forEach, eso ya te da una pista de lo que está ocurriendo

js
const wait = ms => new Promise((r, j) => setTimeout(r, ms))

const fetchUrl = async url => {
await wait(1000)
return 'fantastic ' + url
}

const fetching = () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']
urls.forEach(async url => {
const data = await fetchUrl(url)
console.log(data)
})
}

fetching()

Funciona como esperaríamos?

No

Tenemos los console.log que aparecen al instante y no a cada segundo

Por qué?

Porque todos los console.log se ejecutan a la vez

Por qué?

forEach y map ejecutan una función que recibe como argumento el elemento del array

js
Array.forEach((argumentos) => {})

Bien, pues el problema es que esas funciones las ejecuta en paralelo, no podemos decirle que se espere

Y además, tampoco podemos bloquear el código de todo el forEach completo

Lo puedes comprobar en este ejemplo

js
const wait = ms => new Promise((r, j) => setTimeout(r, ms))

const fetchUrl = async url => {
await wait(5000) // ahora son 5 segundos
return 'fantastic ' + url
}

const fetching = () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']
urls.forEach(async url => {
const data = await fetchUrl(url)
console.log(data)
})

console.log('hemos acabado') // sale al principio!
}

fetching()

Aquí el mensaje hemos acabado nos aparece inmediatamente, y a los 5 segundos nos aparecen los tres fantastic ... a la vez

Es decir, aquí no se bloquea nada

Una opción podría ser ponerle un await delante del forEach, pero tampoco funciona puesto que el forEach no devuelve ninguna promesa

Si en lugar de un forEach utilizamos map, éste nos devuelve una promesa por cada elemento, y podríamos hacer lo siguiente

js
const fetching = async () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']
const promesas = urls.map(async url => {
const data = await fetchUrl(url)
})

await Promise.all(promesas)
console.log('hemos acabado')
}

fetching()

Así conseguimos bloquear el código hasta que termine todo el map, pero no podemos bloquear cada iteración del loop (ocurren en paralelo)

Cómo podemos hacerlo?

Con reduce (tienes una entrada aquí)

Loop con reduce

js
const wait = ms => new Promise((r, j) => setTimeout(r, ms))

const fetchUrl = async url => {
await wait(1000)
return 'fantastic ' + url
}

const fetching = async () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']

await urls.reduce(async (previousPromise, url) => {
await previousPromise
const data = await fetchUrl(url)
console.log(data)
return Promise.resolve()
}, Promise.resolve())

console.log('hemos acabado')
}

fetching()

reduce nos permite incrustar una función en cada ciclo del loop, por lo que podemos implementar una promesa y esperar a que se resuelva, y así bloquear el código como lo hacíamos en el for..of anterior

Y ya lo tenemos

Dicho esto, mejor con reduce o mejor con for..of?

Depende de tu caso

  • Bien podrías definir una función global con reduce, refactorizarla y reutilizarla y así poder disfrutar del paradigma funcional

  • O podrías utilizar for..of y quedarte tan contento, no hay nada malo en mezclar paradigmas

🙋‍♂️

draw of me

Hola, tienes en mente desarrollar una web?

Si quieres, te ayudo

11ty para webs ultra-rápidaseleventy js
Gatsby para webs complejasgatsby js
WordPress para webs para el usuario finalwordpress

Escríbeme

Lista de correo: escribo algo de tanto en cuanto