A lo largo de mi carrera he estado en muchas entrevistas de trabajo, tanto como entrevistador como candidato. He visto que mi parte de candidatos se desmorona cuando se me pide que codifique código asincrónico usando Promises y async / wait.
La programación asincrónica es la esencia de JavaScript, sin embargo, muchos desarrolladores no lo entienden realmente por alguna razón.
Claro, han trabajado con código asíncrono y lo reconocerán cuando lo vean. Saben más o menos cómo funciona. Pero muchos no pueden reproducirlo con precisión y no comprenden todos los detalles esenciales de implementación.
Muchos desarrolladores que he entrevistado están atrapados en este nivel, lo cual es realmente triste.
Si quieres ser un desarrollador de JavaScript serio, entonces la programación asincrónica debería ser una segunda naturaleza. Deberías saber esto como un panadero sabe pan.
La programación asincrónica puede ser complicada, pero no es ciencia espacial. Sólo hay algunas cosas que usted necesita saber.
Cuando domine estos conceptos básicos, no tendrá problemas para comprender los casos de uso más avanzados e implementarlos usted mismo.

Lo que ya sabes

Ya sabes cómo trabajar con Promise y async / wait, al menos eso espero. Si no lo hace, consulte primero estos artículos sobre MDN y luego vuelva.

Lo que deberías saber

Hay algunos patrones en la programación asincrónica en JavaScript que siguen regresando. Estas son soluciones prácticas y de uso múltiple que puede y probablemente aplicará muy a menudo y deberían ser herramientas estándar en su caja de herramientas de JavaScript.

Convertir código basado en devolución de llamada en promesas

El código basado en la devolución de llamada puede ser engorroso para trabajar, especialmente cuando están involucradas múltiples llamadas encadenadas y manejo de errores. Esta es básicamente la razón por la que existen las promesas, para facilitar la programación asincrónica.
Es bastante sencillo convertir el código basado en devolución de llamada en código que usa Promesas.
Echemos un vistazo al código Node.js para leer asincrónicamente un archivo:
const fs = require ('fs'); fs.readFile ('/ ruta / a / archivo', (err, file) => { 
  if (err) { 
    // maneja el error 
  } 
  sino { 
    // haz algo con el archivo 
  } 
});
El fs.readFilemétodo toma la ruta a un archivo y una devolución de llamada como argumentos. Cuando se lee el archivo, la devolución de llamada se invoca con un error como su primer argumento en caso de que algo salga mal, o nullcomo su primer argumento y el contenido del archivo como el segundo argumento en caso de éxito.
Sería bueno si pudiéramos usar este método como una promesa como esta:
fs.readFile ('/ ruta / a / archivo') 
.then (archivo => { 
  // hacer algo con el archivo 
}) 
.catch (err => { 
  // manejar el error 
});
Para lograr esto, podemos envolverlo fácilmente en una Promesa:
const readFileAsync = ruta => { 
  return new Promise ((resolver, rechazar) => { 
    fs.readFile (ruta, (err, archivo) => { 
      return err? rechazar (err): resolver (archivo); 
    }); 
  } ); 
}; // uso 
readFileAsync ('/ ruta / a / archivo') 
.then (archivo => { 
  // hacer algo con el archivo 
}) 
.catch (err => { 
  // manejar el error 
});
La readFileAsyncfunción toma una ruta como argumento y devuelve una nueva Promesa. El constructor Promise toma una llamada función ejecutora que a su vez recibe las devoluciones de llamada resolvereject.
Estas devoluciones de llamada son las que deben invocarse en caso de éxito y fracaso respectivamente. Cuando la devolución de llamada del fs.readFilemétodo envuelto recibe un error, se pasará a rejectCuando filetenga éxito, lo recibido se pasará a resolve.
Si ha prestado mucha atención, habrá notado que las promesas se basan realmente en devoluciones de llamadas .
Así es como puede convertir cualquier función basada en devolución de llamada en una función basada en Promesa.
Consejo adicional: para Node.js probablemente usarías util.promisify.
Puede hacer lo mismo con el código basado en eventos. Por ejemplo con FileReaderDigamos que desea leer un archivo en el navegador y convertirlo en un ArrayBuffer:
const toArrayBuffer = blob => { 
  const reader = new FileReader ();   reader.onload = e => { 
    const buffer = e.target.result; 
  };   reader.onerror = e => { 
    // manejar el error 
  };   reader.readAsArrayBuffer (blob); 
};
También podemos convertir este código basado en eventos en Promesas de la misma manera:
const toArrayBuffer = blob => { 
  const reader = new FileReader ();   return new Promise ((resolver, rechazar) => { 
    reader.onload = e => resolve (e.target.result);     reader.onerror = e => rechazar (e.target.error);     reader.readAsArrayBuffer (blob) ; 
  }); 
};
Aquí básicamente hicimos lo mismo con los eventos en lugar de una devolución de llamada.

Usando resultados intermedios en una cadena Promesa

Si trabaja con Promises por un tiempo, se encontrará con la situación en la que encadena Promesas y necesita usar resultados intermedios que están fuera del alcance de una devolución de llamada de Promise:
const db = openDatabase (); db.getUser (id) 
.then (usuario => db.getOrders (usuario)) 
.then ( pedidos => db.getProducts (pedidos [0])) 
.then (productos => { 
  // ¡no se puede acceder a los pedidos aquí!
 } )
En este ejemplo, abrimos una conexión de base de datos y recuperamos un usuario por su cuenta id, obtenemos los pedidos para ese usuario y luego los productos que están en el primer pedido del usuario.
El problema aquí es que dentro de la devolución de llamada para la Promesa devuelta desde db.getProducts()la ordersvariable no es accesible ya que solo se define en el alcance de la devolución de llamada para la Promesa anterior, devuelta desde db.getOrders().
La solución más simple para esto sería inicializar la ordersvariable en el ámbito externo para que esté disponible en todas partes:
const db = openDatabase (); dejar ordenes; // inicializado en el ámbito externo db.getUser (id) 
.then (user => db.getOrders (user)) 
.then ( orders => db.getProducts (orders [0])) 
.then (products => { 
  // ¡Se puede acceder a los pedidos aquí!
 })
Funciona, pero no es la solución más limpia, especialmente cuando tienes una cadena Promise compleja con muchas variables. Esto daría como resultado una larga lista de variables que deben inicializarse.
En su lugar, debe usar asyncawaitdado que este es uno de los principales casos de uso. Le da el poder de usar código asincrónico con sintaxis sincrónica, por lo que todas las variables comparten el mismo alcance:
const db = openDatabase (); const getUserData = async id => { 
  const user = await db.getUser (id); 
  const orders = await db.getOrders (usuario); 
  const products = await db.getProducts (pedidos [0]);   devolución de productos; 
}; getUserData (123);
Dentro de getUserDatatodas las variables ahora comparten el mismo alcance y todas son accesibles en ese alcance, sin embargo, el código es completamente asíncrono. La llamada a getUserDatano bloqueará ningún código siguiente.
Este es un ejemplo donde asyncawaitrealmente brilla.

Puedes combinar async / await con Promises

¿Cómo obtendría los productos devueltos getUserDataen el ejemplo anterior?
Como getUserDataes una asyncfunción que usarías awaitdentro de otra asyncfunción:
const getProducts = async () => { 
  productos const = await getUserData (123); 
};
Pero también podrías usar .then:
getUserData (123) 
.then (productos => { 
  // use productos 
})
Esto funciona porque una asyncfunción siempre devuelve una promesa implícita .
Si ha prestado mucha atención, habrá notado que en async/await realidad se basa en Promesas.

Lo que es bueno saber

Como siempre, el diablo está en los detalles y esto también es cierto para la programación asincrónica en JavaScript. A menudo he visto a los desarrolladores perder detalles esenciales de implementación durante las entrevistas de trabajo, lo que demuestra una comprensión deficiente de los conceptos.

Las devoluciones de llamada de promesa siempre devuelven una promesa

Cuando se encadenan Promesas, generalmente se devuelve una Promesa de todos .thenen la cadena:
db.getUser (id) 
.then (usuario => db.getOrders (usuario) ) 
.then (pedidos => db.getProducts (pedidos [0]) ) 
.then (productos => db.getStats (productos) )
En el ejemplo anterior de una promesa se devuelve desde cada devolución de llamada en el interior .then, lo que significa que db.getOrdersdb.getProductsdb.getStatstodos vuelven una promesa.
Pero cuando tiene una cadena compleja, tarde o temprano deberá devolver algo que no sea una Promesa:
db.getUser (id) 
.then (user => db.getOrders (user)) 
.then (orders => db.getProducts (orders [0])) 
.then (products => products.length) // <- ¡Uy, no es una promesa! 
.then (numberOfProducts => { 
  db.saveStats (numberOfProducts);   return db.getStats (productos); 
}) 
...
Sin embargo, el código anterior se ejecutará perfectamente bien ya que cualquier valor de retorno de una devolución de llamada de Promise en el interior .theno se .catchincluirá automáticamente en una Promesa.
Esto significa que puede devolver valores arbitrarios dentro de una cadena de Promesas.

.then puede tomar dos argumentos

Normalmente, simplemente pasaría un argumento a .thenla devolución de llamada que debería llamarse cuando se resuelva la Promesa. La devolución de llamada a la que se debe llamar cuando se rechaza la promesa se transfiere a .catch.
Pero en .thenrealidad puede tomar estas dos devoluciones de llamada, la primera para cuando la Promesa se resuelve (éxito) y la segunda para cuando la Promesa rechaza (error).
Entonces, en lugar de esto:
fetch ('http://some.domain.com') 
.then (respuesta => console.log ('success')) 
.catch (err => console.error ('error'))
También puedes hacer esto:
fetch ('http://some.domain.com') 
.then (respuesta => console.log ('success'), 
      err => console.error ('error') )
Podrías, pero no deberías . Siempre.
Usted debe siempre utilizar .catchen una cadena promesa para detectar errores.
La diferencia entre .catchy la devolución de llamada de error como segundo argumento .thenes que si se produce un error en una devolución de llamada exitosa en cualquier parte de la cadena de Promise, será detectado por .catch:
fetch ('http://some.domain.com') 
.then (response => response.json ()) 
.then (json => fetch (...)) 
.then (response => response.text () ) 
.then (text => ...) 
.catch (err => console.error (err))   // cualquier error se detectará aquí
Si pasa una devolución de llamada tanto éxito y una respuesta de error para .theny se produce un error en la devolución de llamada de éxito, que no va a ser atrapado por la devolución de llamada de error:
fetch ('http://some.domain.com') 
.then (successCallback, 
      errorCallback ) // un error en successCallback NO irá aquí
No hagas esto, siempre úsalo .catch.

Lo que quizás no sepas

La programación asincrónica puede, por supuesto, ser mucho más compleja y exigir escenarios más complejos. He pedido a los candidatos en entrevistas de trabajo que codifiquen un escenario bastante simple en el que una llamada API asincrónica se debe realizar condicionalmente.

Llamada API asincrónica condicional

Digamos que una llamada a la API se debe hacer solo cuando el usuario ha iniciado sesión y luego, después de eso, se debe ejecutar algún código para eliminar la sesión del usuario. Pero la eliminación de la sesión solo se puede hacer una vez finalizada la llamada a la API. Si el usuario no ha iniciado sesión, se omite la llamada a la API y la sesión se elimina inmediatamente.
Cuando el usuario inicia sesión, simplemente podemos esperar a que se resuelva la Promesa devuelta por la llamada a la API y luego eliminar la sesión dentro de la devolución de llamada pasada a .then:
if (userIsLoggedIn) { 
  apiCall () 
  .then (res => { 
    deleteSession () 
  }) 
}
Si el usuario no ha iniciado sesión, omitimos la llamada API y vamos directamente a deleteSessionPero como la llamada a la API es asíncrona, cualquier código posterior se ejecutará inmediatamente, por lo que debemos duplicar la llamada a deleteSession:
const checkLogin = () => { 
  if (userIsLoggedIn) { 
    apiCall () 
    .then (res => { 
      deleteSession () 
    }) 
  } 
  else { 
    deleteSession (); 
  } };
Ahora tenemos dos llamadas a las deleteSessionque no es bueno. Como apiCalles asíncrono, no tenemos más remedio que ejecutar deleteSessiondentro .theny también crear una Promesa para cuando el usuario no está conectado:
const checkLogin = () => { 
  if (userIsLoggedIn) { 
    return apiCall (); 
  } 
  else { 
    return Promise.resolve (verdadero); // funciona, pero esto no es bueno 
  }
 }; checkLogin () 
.then (() => { 
  deleteSession (); 
})
La llamada duplicada a deleteSessionse ha ido, pero ahora tenemos que devolver una promesa inútil de checkLogincuando el usuario no ha iniciado sesión, solo para que el código se ejecute en el orden correcto.
Esta es la solución que he visto a menudo en el código de producción, pero definitivamente es un olor a código y debe evitarse.
Este ejemplo sigue siendo bastante simple, pero cuando el código se vuelve más complejo, puede convertirse en un código difícil de seguir y difícil de seguir.
La única forma de resolver esto de una manera limpia y concisa es usar asyncawait:
const checkLogin = asíncrono () => { 
  si (userIsLoggedIn) { 
    esperar apiCall (); 
  }   deleteSession (); 
};
Ahora cuando el usuario haya iniciado sesión apiCallse ejecutará. Como checkLoginahora es una asyncfunción y apiCalltiene el prefijo await, ahora esperará hasta apiCallque termine y luego se ejecutará deleteSession.
Cuando el usuario no haya iniciado sesión, apiCallsimplemente se omitirá y solo deleteSessionse ejecutará. No más códigos duplicados, no más promesas inútiles.
Ese es el poder de asyncawait.

Una promesa con un tiempo de espera

Otro escenario común es cuando se debe establecer un tiempo de espera en una (posiblemente) llamada de API de larga ejecución.
Digamos que llamas a una API que tiene que responder en 5 segundos. Si no es así, se debe devolver un mensaje para indicar que la llamada tardó demasiado y se agotó el tiempo de espera.
He visto a los desarrolladores luchar con esto mientras se enredan en múltiples espaguetis de Promise. La solución es bastante simple de implementar Promise.race.
Promise.racetoma una matriz (u otra Iterable) de Promesas y resuelve o rechaza tan pronto como una de las Promesas en la matriz resuelve o rechaza. Puede usar esto para pasar la Promesa a la que desea aplicar un tiempo de espera junto con otra Promesa que se resuelve / rechaza después de ese tiempo Promise.race.
Entonces, si su Promesa se resuelve dentro del tiempo de espera especificado, todo está bien. Pero si no es así, la segunda Promesa se resolverá o rechazará después del tiempo de espera y señalará que hubo un tiempo de espera:
const apiCall = url => buscar (url); const timeout = time => { 
  return new Promise ((resolver, rechazar) => { 
    setTimeout (() => rechazar ('Promise timed out'), time); 
  }); 
}; Promise.race ([apiCall, timeout]) 
.then (respuesta => { 
  // la respuesta de apiCall fue exitosa 
}) 
.catch (err => { 
  // manejar el tiempo de espera 
});
Sin embargo, Aviso que cualquier otro error que pudiera ocurrir en apiCallserá también ir al .catchcontrolador, por lo que necesita para ser capaz de determinar si el error es el tiempo de espera o un error real.

La clave para comprender la programación asincrónica en JavaScript

Para comprender realmente la programación asincrónica, debe comprender los conceptos básicos, los fundamentos sobre los que se construyó.
Recuerda:
async / await se basa en promesas
Las promesas se basan en devoluciones de llamada
las devoluciones de llamada son la base de la programación asincrónica en JavaScript
Demasiados desarrolladores que entrevisté solo tienen una comprensión vaga o superficial de cómo funciona más o menos, pero esto no es suficiente.
Solo si realmente comprende la base puede comprender realmente y, en última instancia, dominar la programación asincrónica en JavaScript.