try / catch blocs avec async / await


116

Je creuse dans la fonction async / await du nœud 7 et je continue de trébucher sur du code comme celui-ci

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();

Cela semble être la seule possibilité de résoudre / rejeter ou retourner / lancer avec async / await, cependant, la v8 n'optimise pas le code dans les blocs try / catch?!

Existe-t-il des alternatives?


Que signifie «lancer après qu'une attente n'a pas réussi»? Si ça fait des erreurs? S'il ne renvoie pas le résultat attendu? Vous pouvez relancer le bloc catch.
DevDig

afaik v8 optimise try / catch, une déclaration de lancer est la plus lente
Tamas Hegedus

1
Je ne comprends toujours pas la question. Vous pouvez utiliser le chaînage de vieilles promesses, mais je ne pense pas que ce serait plus rapide. Vous êtes donc préoccupé par les performances de try-catch? Alors qu'est-ce que cela a à voir avec async wait?
Tamas Hegedus

Vérifiez ma réponse J'ai essayé d'obtenir une approche plus propre
zardilior

Ici, vous pouvez faire ceci stackoverflow.com/a/61833084/6482248 Il semble plus propre
Prathamesh More

Réponses:


133

Alternatives

Une alternative à ceci:

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

serait quelque chose comme ça, en utilisant explicitement les promesses:

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

ou quelque chose comme ça, en utilisant le style de passage continu:

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

Exemple original

Ce que fait votre code d'origine est de suspendre l'exécution et d'attendre que la promesse retournée soit getQuote()réglée. Il continue ensuite l'exécution et écrit la valeur renvoyée dans var quote, puis l'imprime si la promesse a été résolue, ou lève une exception et exécute le bloc catch qui imprime l'erreur si la promesse a été rejetée.

Vous pouvez faire la même chose en utilisant l'API Promise directement comme dans le deuxième exemple.

Performance

Maintenant, pour la performance. Testons-le!

Je viens d'écrire ce code - f1()donne 1comme valeur de retour, f2()jette 1comme exception:

function f1() {
  return 1;
}

function f2() {
  throw 1;
}

Appelons maintenant le même code des millions de fois, d'abord avec f1():

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

Et puis passons f1()à f2():

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

Voici le résultat que j'ai obtenu pour f1:

$ time node throw-test.js 
1000000

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

Voici ce que j'ai pour f2:

$ time node throw-test.js 
1000000

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

Il semble que vous puissiez faire quelque chose comme 2 millions de lancers par seconde dans un processus à un seul thread. Si vous faites plus que cela, vous devrez peut-être vous en inquiéter.

Résumé

Je ne m'inquiéterais pas des choses comme ça dans Node. Si des choses comme ça sont beaucoup utilisées, elles seront finalement optimisées par les équipes V8 ou SpiderMonkey ou Chakra et tout le monde suivra - ce n'est pas comme si ce n'était pas optimisé en tant que principe, ce n'est tout simplement pas un problème.

Même s'il n'est pas optimisé, je dirais toujours que si vous maximisez votre processeur dans Node, vous devriez probablement écrire votre calcul numérique en C - c'est à cela que servent les addons natifs, entre autres choses. Ou peut-être que des choses comme node.native conviendraient mieux à ce travail que Node.js.

Je me demande quel serait un cas d'utilisation qui aurait besoin de tant d'exceptions. En général, lever une exception au lieu de renvoyer une valeur est, eh bien, une exception.


Je sais que le code peut facilement être écrit avec Promises, comme mentionné, je l'ai vu sur divers exemples, c'est pourquoi je demande. Avoir une seule opération dans try / catch peut ne pas être un problème, mais plusieurs fonctions async / wait avec une logique d'application supplémentaire peuvent l'être.
Patrick

4
@Patrick "pourrait être" et "sera" est une différence entre la spéculation et le test réel. Je l'ai testé pour une seule déclaration car c'est ce qui était dans votre question, mais vous pouvez facilement convertir mes exemples pour tester plusieurs déclarations. J'ai également fourni plusieurs autres options pour écrire du code asynchrone que vous avez également demandé. Si cela répond à votre question, vous pouvez envisager d' accepter la réponse . Pour résumer: bien sûr, les exceptions sont plus lentes que les retours mais leur utilisation doit être une exception.
rsp

1
Lancer une exception est en effet censé être une exception. Cela étant dit, le code n'est pas optimisé, que vous lanciez une exception ou non. L'atteinte des performances provient de l'utilisation try catchet non de la levée d'une exception. Bien que les chiffres soient petits, c'est presque 10 fois plus lent selon vos tests, ce qui n'est pas anodin.
Nepoxx

22

Alternative similaire à la gestion des erreurs dans Golang

Comme async / await utilise des promesses sous le capot, vous pouvez écrire une petite fonction utilitaire comme celle-ci:

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

Ensuite, importez-le chaque fois que vous avez besoin de détecter des erreurs et encapsulez votre fonction asynchrone qui renvoie une promesse avec elle.

import catchEm from 'utility';

async performAsyncWork() {
  const [err, data] = await catchEm(asyncFunction(arg1, arg2));
  if (err) {
    // handle errors
  } else {
    // use data
  }
}

J'ai créé un package NPM qui fait exactement ce qui précède - npmjs.com/package/@simmo/task
Mike

2
@Mike Vous réinventez peut-être la roue - il existe déjà un package populaire qui fait exactement cela: npmjs.com/package/await-to-js
Jakub Kukul

21

Une alternative au bloc try-catch est await-to-js lib. Je l'utilise souvent. Par exemple:

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);

}

Cette syntaxe est beaucoup plus propre par rapport à try-catch.


J'ai essayé et j'ai adoré. Code propre et lisible au détriment de l'installation d'un nouveau module. Mais si vous prévoyez d'écrire beaucoup de fonctions asynchrones, je dois dire que c'est un excellent ajout! Merci
filipbarak

15
async function main() {
  var getQuoteError
  var quote = await getQuote().catch(err => { getQuoteError = err }

  if (getQuoteError) return console.error(err)

  console.log(quote)
}

Au lieu de déclarer une variable possible pour contenir une erreur en haut, vous pouvez faire

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

Bien que cela ne fonctionnera pas si quelque chose comme une erreur TypeError ou Reference est renvoyé. Vous pouvez vous assurer qu'il s'agit d'une erreur régulière avec

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)
}

Ma préférence pour cela est de tout emballer dans un gros bloc try-catch où plusieurs promesses sont créées, ce qui peut rendre difficile la gestion de l'erreur spécifiquement à la promesse qui l'a créée. L'alternative étant plusieurs blocs try-catch que je trouve tout aussi encombrants


8

Une alternative plus propre serait la suivante:

En raison du fait que chaque fonction asynchrone est techniquement une promesse

Vous pouvez ajouter des captures aux fonctions lors de leur appel avec wait

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'))

Pas besoin d'essayer catch, car toutes les erreurs de promesses sont gérées, et vous n'avez aucune erreur de code, vous pouvez l'omettre dans le parent !!

Disons que vous travaillez avec mongodb, s'il y a une erreur, vous préférerez peut-être la gérer dans la fonction qui l'appelle plutôt que de créer des wrappers ou d'utiliser des try catch.


Vous avez 3 fonctions. L'un récupère les valeurs et attrape l'erreur, un autre vous renvoie s'il n'y a pas d'erreur et enfin un appel à la première fonction avec un rappel pour vérifier si celle-ci a renvoyé une erreur. Tout ceci est résolu par un seul bloc "promise" .then (cb) .catch (cb) ou trycatch.
Chief Koshi

@Chiefkoshi Comme vous pouvez le voir, une seule capture ne ferait pas l'affaire car l'erreur est gérée différemment dans les trois cas. Si le premier échoue, il renvoie d (), si le second échoue, il retourne null si le dernier échoue, un message d'erreur différent est affiché. La question demande des erreurs de gestion lors de l'utilisation de wait. C'est donc aussi la réponse. Tout devrait s'exécuter si l'un d'entre eux échoue. Essayer les blocs de capture en exigerait trois dans cet exemple particulier qui n'est pas plus propre
zardilior

1
La question ne demande pas d'être exécutée après des promesses ratées. Ici, vous attendez B, puis exécutez C et renvoyez D s'ils ont commis une erreur. Comment est ce nettoyant? C doit attendre B mais ils sont indépendants l'un de l'autre. Je ne vois pas pourquoi ils seraient ensemble dans A s'ils sont indépendants. S'ils étaient dépendants l'un de l'autre, vous voudriez arrêter l'exécution de C si B échoue, le travail de .then.catch ou try-catch. Je suppose qu'ils ne renvoient rien et effectuent des actions asynchrones sans aucun rapport avec A. Pourquoi sont-ils appelés avec async wait?
Chief Koshi

La question concerne les alternatives pour essayer des blocs catch pour gérer les erreurs lors de l'utilisation de async / await. L'exemple ici doit être descriptif et n'est qu'un exemple. Il montre la gestion individuelle des opérations indépendantes de manière séquentielle, ce qui correspond généralement à l'utilisation de async / await. Pourquoi sont-ils appelés avec async await, c'est juste pour montrer comment cela pourrait être géré. Son descriptif plus que justifié.
zardilior

2

J'aimerais faire de cette façon :)

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);
});

C'est similaire à la gestion des erreurs avec co

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

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

  }).catch(err => {
    console.error(err);
  });
};

Le code n'est pas très clair, l'homme semble intéressant, pouvez-vous le modifier?
zardilior

Il est regrettable qu'il n'y ait pas d'explication dans cette réponse car elle démontre en fait un excellent moyen d'éviter de tenter de rattraper chaque const que vous assignez await!
Jim le

0

catchde cette façon, d'après mon expérience, est dangereux. Toute erreur lancée dans la pile entière sera interceptée, pas seulement une erreur de cette promesse (ce qui n'est probablement pas ce que vous voulez).

Le deuxième argument d'une promesse est déjà un rappel de rejet / échec. Il est préférable et plus sûr de l'utiliser à la place.

Voici une ligne unique dactylographiée que j'ai écrite pour gérer cela:

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()
);
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.