probar / atrapar bloques con async / await

116

Estoy profundizando en la función async / await del nodo 7 y sigo tropezando con un código como este

function getQuote() {
  let quote = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
  return quote;
}

async function main() {
  try {
    var quote = await getQuote();
    console.log(quote);
  } catch (error) {
    console.error(error);
  }
}

main();

Esta parece ser la única posibilidad de resolver / rechazar o devolver / lanzar con async / await, sin embargo, ¿v8 no optimiza el código dentro de los bloques try / catch?

¿Hay alternativas?

Patricio
fuente
¿Qué significa "lanzar después de que una espera no tiene éxito"? ¿Si hay errores? ¿Si no devuelve el resultado esperado? Podría volver a lanzar en el bloque de captura.
DevDig
afaik v8 optimiza try / catch, una declaración de lanzamiento es la lenta
Tamas Hegedus
1
Todavía no entiendo la pregunta. Van a usar el antiguo encadenamiento de promesas, pero no creo que sea más rápido. Entonces, ¿le preocupa el rendimiento de try-catch? Entonces, ¿qué tiene que ver con async await?
Tamas Hegedus
Verifique mi respuesta Traté de obtener un enfoque más limpio
zardilior
Aquí puede hacer esto stackoverflow.com/a/61833084/6482248 Se ve más limpio
Prathamesh More

Respuestas:

133

Alternativas

Una alternativa a esto:

async function main() {
  try {
    var quote = await getQuote();
    console.log(quote);
  } catch (error) {
    console.error(error);
  }
}

sería algo como esto, usando promesas explícitamente:

function main() {
  getQuote().then((quote) => {
    console.log(quote);
  }).catch((error) => {
    console.error(error);
  });
}

o algo como esto, usando el estilo de paso de continuación:

function main() {
  getQuote((error, quote) => {
    if (error) {
      console.error(error);
    } else {
      console.log(quote);
    }
  });
}

Ejemplo original

Lo que hace su código original es suspender la ejecución y esperar getQuote()a que se resuelva la promesa devuelta por . Luego continúa la ejecución y escribe el valor devuelto var quotey luego lo imprime si la promesa se resolvió, o lanza una excepción y ejecuta el bloque catch que imprime el error si la promesa fue rechazada.

Puede hacer lo mismo usando la API de Promise directamente como en el segundo ejemplo.

Actuación

Ahora, para la actuación. ¡Probémoslo!

Acabo de escribir este código: f1()da 1como valor de retorno, f2()arroja 1como excepción:

function f1() {
  return 1;
}

function f2() {
  throw 1;
}

Ahora llamemos al mismo código millones de veces, primero con f1():

var sum = 0;
for (var i = 0; i < 1e6; i++) {
  try {
    sum += f1();
  } catch (e) {
    sum += e;
  }
}
console.log(sum);

Y luego cambiemos f1()a f2():

var sum = 0;
for (var i = 0; i < 1e6; i++) {
  try {
    sum += f2();
  } catch (e) {
    sum += e;
  }
}
console.log(sum);

Este es el resultado que obtuve para f1:

$ time node throw-test.js 
1000000

real    0m0.073s
user    0m0.070s
sys     0m0.004s

Esto es lo que obtuve por f2:

$ time node throw-test.js 
1000000

real    0m0.632s
user    0m0.629s
sys     0m0.004s

Parece que puede hacer algo así como 2 millones de lanzamientos por segundo en un proceso de un solo subproceso. Si está haciendo más que eso, es posible que deba preocuparse por ello.

Resumen

No me preocuparía por cosas así en Node. Si este tipo de cosas se usan mucho, los equipos de V8, SpiderMonkey o Chakra lo optimizarán eventualmente y todos lo seguirán; no es que no esté optimizado como principio, simplemente no es un problema.

Incluso si no está optimizado, todavía diría que si está maximizando su CPU en Node, probablemente debería escribir su número en C; para eso son los complementos nativos, entre otras cosas. O tal vez cosas como node.native serían más adecuadas para el trabajo que Node.js.

Me pregunto cuál sería un caso de uso que necesita lanzar tantas excepciones. Por lo general, lanzar una excepción en lugar de devolver un valor es, bueno, una excepción.

rsp
fuente
Sé que el código se puede escribir fácilmente con Promises, como se mencionó, lo he visto en varios ejemplos, por eso estoy preguntando. Tener una sola operación dentro de try / catch puede no ser un problema, pero sí pueden serlo varias funciones async / await con más lógica de aplicación.
Patrick
4
@Patrick "podría ser" y "será" es una diferencia entre la especulación y la prueba real. Lo probé para una sola declaración porque eso es lo que estaba en su pregunta, pero puede convertir fácilmente mis ejemplos para probar varias declaraciones. También proporcioné varias otras opciones para escribir código asincrónico sobre las que también preguntaste. Si responde a su pregunta, puede considerar aceptar la respuesta . En resumen: por supuesto, las excepciones son más lentas que las devoluciones, pero su uso debería ser una excepción.
rsp
1
De hecho, se supone que lanzar una excepción es una excepción. Dicho esto, el código no está optimizado tanto si lanza una excepción como si no. El impacto en el rendimiento proviene del uso try catch, no de lanzar una excepción. Si bien los números son pequeños, es casi 10 veces más lento según sus pruebas, lo cual no es insignificante.
Nepoxx
21

Alternativa similar al manejo de errores en Golang

Debido a que async / await usa promesas bajo el capó, puede escribir una pequeña función de utilidad como esta:

export function catchEm(promise) {
  return promise.then(data => [null, data])
    .catch(err => [err]);
}

Luego impórtelo siempre que necesite detectar algunos errores y envuelva su función asíncrona que devuelve una promesa con ella.

import catchEm from 'utility';

async performAsyncWork() {
  const [err, data] = await catchEm(asyncFunction(arg1, arg2));
  if (err) {
    // handle errors
  } else {
    // use data
  }
}
Steve Banton
fuente
Creé
Mike
2
@Mike Puede que estés reinventando la rueda; ya existe un paquete popular que hace exactamente esto: npmjs.com/package/await-to-js
Jakub Kukul
21

Una alternativa al bloque try-catch es await-to-js lib. Yo lo uso a menudo. Por ejemplo:

import to from 'await-to-js';

async function main(callback) {
    const [err,quote] = await to(getQuote());
    
    if(err || !quote) return callback(new Error('No Quote found'));

    callback(null,quote);

}

Esta sintaxis es mucho más limpia en comparación con try-catch.

Pulkit chadha
fuente
Probé esto y me encantó. Código limpio y legible a expensas de instalar un módulo nuevo. Pero si planeas escribir muchas funciones asincrónicas, debo decir que esta es una gran adición. Gracias
filipbarak
15
async function main() {
  var getQuoteError
  var quote = await getQuote().catch(err => { getQuoteError = err }

  if (getQuoteError) return console.error(err)

  console.log(quote)
}

Alternativamente, en lugar de declarar una posible var para mantener un error en la parte superior, puede hacer

if (quote instanceof Error) {
  // ...
}

Aunque eso no funcionará si se lanza algo como un error de tipo o un error de referencia. Puede asegurarse de que es un error regular aunque con

async function main() {
  var quote = await getQuote().catch(err => {
    console.error(err)      

    return new Error('Error getting quote')
  })

  if (quote instanceOf Error) return quote // get out of here or do whatever

  console.log(quote)
}

Mi preferencia por esto es envolver todo en un gran bloque try-catch donde se crean múltiples promesas que pueden hacer que sea engorroso manejar el error específicamente con la promesa que lo creó. Con la alternativa de ser múltiples bloques try-catch que encuentro igualmente engorrosos

Tony
fuente
8

Una alternativa más limpia sería la siguiente:

Debido al hecho de que cada función asincrónica es técnicamente una promesa

Puede agregar capturas a las funciones cuando las llama con await

async function a(){
    let error;

    // log the error on the parent
    await b().catch((err)=>console.log('b.failed'))

    // change an error variable
    await c().catch((err)=>{error=true; console.log(err)})

    // return whatever you want
    return error ? d() : null;
}
a().catch(()=>console.log('main program failed'))

No es necesario intentar la captura, ya que se manejan todos los errores de promesas y no tiene errores de código, ¡puede omitir eso en el padre!

Digamos que está trabajando con mongodb, si hay un error, es posible que prefiera manejarlo en la función que lo llama en lugar de hacer envoltorios o usar try catches.

zardilior
fuente
Tienes 3 funciones. Uno obtiene valores y detecta el error, otro devuelve si no hay error y finalmente una llamada a la primera función con una devolución de llamada para verificar si esa devolvió un error. Todo esto se resuelve con una única "promesa". Luego (cb) .catch (cb) o bloque trycatch.
Chief koshi
@Chiefkoshi Como puede ver, una sola captura no funcionaría ya que el error se maneja de manera diferente en los tres casos. Si el primero falla, devuelve d (), si el segundo falla, devuelve nulo si el último falla, se muestra un mensaje de error diferente. La pregunta solicita el manejo de errores al usar await. Así que esa es también la respuesta. Todos deberían ejecutarse si alguno falla. Probar los bloques de captura requeriría tres de ellos en este ejemplo particular que no es más limpio
zardilior
1
La pregunta no pide la ejecución después de promesas fallidas. Aquí esperas a B, luego ejecutas C y devuelves D si cometieron errores. ¿Cómo es este limpiador? C tiene que esperar a B, pero son independientes entre sí. No veo ninguna razón por la que estarían juntos en A si fueran independientes. Si fueran dependientes el uno del otro, querría detener la ejecución de C si B falla, el trabajo de. Then.catch o try-catch. Supongo que no devuelven nada y realizan algunas acciones asincrónicas completamente ajenas a A. ¿Por qué se llaman con async await?
Chief koshi
La pregunta es sobre alternativas para intentar atrapar bloques para manejar errores al usar async / await. El ejemplo aquí debe ser descriptivo y no es más que un ejemplo. Muestra el manejo individual de operaciones independientes de forma secuencial, que suele ser la forma en que se usan async / await. Por qué se llaman con async await, es solo para mostrar cómo se puede manejar. Es descriptivo más que justificado.
zardilior
2

Me gustaría hacerlo de esta manera :)

const sthError = () => Promise.reject('sth error');

const test = opts => {
  return (async () => {

    // do sth
    await sthError();
    return 'ok';

  })().catch(err => {
    console.error(err); // error will be catched there 
  });
};

test().then(ret => {
  console.log(ret);
});

Es similar a manejar errores con co

const test = opts => {
  return co(function*() {

    // do sth
    yield sthError();
    return 'ok';

  }).catch(err => {
    console.error(err);
  });
};
Cooper Hsiung
fuente
El código no es muy claro, pero parece interesante, ¿podrías editar?
zardilior
Es desafortunado que no haya explicación en esta respuesta porque en realidad demuestra una excelente manera de evitar la captura de intentos de cada constante que asigne await.
Jim
0

catchEn mi experiencia, actuar de esta manera es peligroso. Se detectará cualquier error arrojado en toda la pila, no solo un error de esta promesa (que probablemente no sea lo que desea).

El segundo argumento de una promesa ya es una devolución de llamada de rechazo / falla. Es mejor y más seguro usar eso en su lugar.

Aquí hay un one-line mecanografiado seguro que escribí para manejar esto:

function wait<R, E>(promise: Promise<R>): [R | null, E | null] {
  return (promise.then((data: R) => [data, null], (err: E) => [null, err]) as any) as [R, E];
}

// Usage
const [currUser, currUserError] = await wait<GetCurrentUser_user, GetCurrentUser_errors>(
  apiClient.getCurrentUser()
);
sarink
fuente