Comment rejeter en syntaxe asynchrone / wait?

283

Comment puis-je rejeter une promesse renvoyée par une fonction asynchrone / attendre?

par exemple à l'origine

foo(id: string): Promise<A> {
  return new Promise((resolve, reject) => {
    someAsyncPromise().then((value)=>resolve(200)).catch((err)=>reject(400))
  });
}

Traduire en asynchrone / attendre

async foo(id: string): Promise<A> {
  try{
    await someAsyncPromise();
    return 200;
  } catch(error) {//here goes if someAsyncPromise() rejected}
    return 400; //this will result in a resolved promise.
  });
}

Alors, comment pourrais-je correctement rejeter cette promesse dans ce cas?

Phénix
la source
20
Évitez l' Promiseanti-modèle constructeur ! Même le premier extrait aurait dû être écritfoo(id: string): Promise<A> { return someAsyncPromise().then(()=>{ return 200; }, ()=>{ throw 400; }); }
Bergi
10
Je pense qu'il serait utile de traduire le code de cette question en vanilla JS, car la question n'a rien à voir avec TypeScript. Si je le faisais, cette modification serait-elle probablement acceptée?
Jacob Ford

Réponses:

328

Votre meilleur pari est d' throwune Errorenveloppe la valeur, qui se traduit par une promesse rejetée avec une Errorenveloppe la valeur:

} catch (error) {
    throw new Error(400);
}

Vous pouvez également juste throwla valeur, mais il n'y a pas d'informations de trace de pile:

} catch (error) {
    throw 400;
}

Alternativement, renvoyez une promesse rejetée avec un Errorencapsulage de la valeur, mais ce n'est pas idiomatique:

} catch (error) {
    return Promise.reject(new Error(400));
}

(Ou tout simplement return Promise.reject(400);, mais encore une fois, il n'y a pas d'informations contextuelles.)

(Dans votre cas, comme vous utilisez TypeScriptet que foola valeur retrn est Promise<A>, vous utiliseriez return Promise.reject<A>(400 /*or error*/);)

Dans une situation async/ await, ce dernier est probablement un peu une mauvaise correspondance sémantique, mais cela fonctionne.

Si vous lancez un Error, cela joue bien avec tout ce qui consomme votre foorésultat avec la awaitsyntaxe:

try {
    await foo();
} catch (error) {
    // Here, `error` would be an `Error` (with stack trace, etc.).
    // Whereas if you used `throw 400`, it would just be `400`.
}
TJ Crowder
la source
12
Et puisque async / wait consiste à ramener le flux asynchrone à la syntaxe de synchronisation, throwc'est mieux que Promise.reject()IMO. Que ce soit ou non throw 400est une question différente. Dans le PO, il rejette 400, et nous pouvons dire qu'il devrait Errorplutôt rejeter un .
unional
2
Oui, cependant, si votre chaîne de code utilise vraiment async / wait, alors vous aurez du mal à taper ici, permettez-moi de faire une démonstration comme réponse
unional
1
y a-t-il une raison pour laquelle vous voudriez lancer une nouvelle erreur par opposition à l'erreur qui vous a été donnée dans le bloc catch?
Adrian M
1
@sebastian - Je ne sais pas ce que tu veux dire par là. Dans les asyncfonctions, il n'y a pas de fonction resolveou reject. Il y a returnet throw, qui sont les moyens idiomatiques pour résoudre et rejeter la asyncpromesse de la fonction.
TJ Crowder
1
@ Jan-PhilipGehrcke - Vous le pouvez , mais je ne le fais jamais. Cela crée une instance, newrend cela explicite. Notez également que vous ne pouvez pas le laisser de côté si vous avez une Errorsous-classe ( class MyError extends Error), alors ...
TJ Crowder
146

Il convient également de mentionner que vous pouvez simplement enchaîner une catch()fonction après l'appel de votre opération asynchrone car sous le capot, une promesse est toujours retournée.

await foo().catch(error => console.log(error));

De cette façon, vous pouvez éviter la try/catchsyntaxe si vous ne l'aimez pas.

David
la source
1
Donc, si je veux rejeter ma asyncfonction, je lève une exception, puis je l'attrape bien .catch()comme si je revenais Promise.rejectou appelais reject. Je l'aime!
icl7126
7
Je ne comprends pas pourquoi cela devrait être la réponse acceptée. Non seulement le nettoyeur de réponse est accepté, mais il gère également toutes les awaitdéfaillances possibles en une seule routine. À moins que des cas très spécifiques soient nécessaires pour chacun, awaitje ne vois pas pourquoi vous voudriez les attraper comme ça. Juste moi humble avis.
edgaralienfoe
1
@jablesauce pour mon cas d'utilisation, non seulement je devais attraper chaque awaitéchec séparément, mais je devais également travailler avec un cadre basé sur des promesses qui rejetait les promesses en cas d'erreur.
Reuven Karasik
Ça n'a pas marché pour moi. Ne semble pas entrer dans le bloc catch si l'url échoue. [réponse] = attendre oauthGet ( ${host}/user/permissions/repositories_wrong_url/, accessToken, accessTokenSecret) .catch (err => {logger.error ('Impossible de récupérer les autorisations du référentiel', err); callback (err);})
sn.anurag
1
n'a pas besoin de awaitmot clé ici.
Ashish Rawat
12

Vous pouvez créer une fonction wrapper qui accepte une promesse et renvoie un tableau avec des données si aucune erreur et l'erreur s'il y a eu une erreur.

function safePromise(promise) {
  return promise.then(data => [ data ]).catch(error => [ null, error ]);
}

Utilisez-le comme ceci dans ES7 et dans une fonction asynchrone :

async function checkItem() {
  const [ item, error ] = await safePromise(getItem(id));
  if (error) { return null; } // handle error and return
  return item; // no error so safe to use item
}
Andy
la source
1
On dirait une tentative d'avoir la belle syntaxe Go mais sans beaucoup d'élégance. Je trouve que le code qui l'utilise est obscurci juste assez pour aspirer la valeur de la solution.
Kim
8

Une meilleure façon d'écrire la fonction asynchrone serait de renvoyer une promesse en attente depuis le début, puis de traiter les rejets et les résolutions dans le rappel de la promesse, plutôt que de simplement cracher une promesse rejetée en cas d'erreur. Exemple:

async foo(id: string): Promise<A> {
    return new Promise(function(resolve, reject) {
        // execute some code here
        if (success) { // let's say this is a boolean value from line above
            return resolve(success);
        } else {
            return reject(error); // this can be anything, preferably an Error object to catch the stacktrace from this function
        }
    });
}

Ensuite, vous enchaînez simplement les méthodes sur la promesse retournée:

async function bar () {
    try {
        var result = await foo("someID")
        // use the result here
    } catch (error) {
        // handle error here
    }
}

bar()

Source - ce tutoriel:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

OzzyTheGiant
la source
5
La question posée spécifiquement sur l'utilisation de async / wait. Ne pas utiliser les promesses
Mak
Cette réponse n'était pas censée être la bonne réponse définitive. C'était une réponse de soutien aux autres réponses données ci-dessus. Je l'aurais mis en commentaire mais étant donné que j'ai du code, le champ de réponse est un meilleur endroit.
OzzyTheGiant
Merci de clarifier. Il est certainement utile de montrer comment créer une fonction asynchrone. La mise à jour du deuxième bloc de code à utiliser en attente sera beaucoup plus pertinente et utile. Cheers
Mak
J'ai modifié votre réponse pour la mettre à jour. Faites-moi savoir si j'ai raté quelque chose
Mak
4

J'ai une suggestion pour gérer correctement les rejets dans une nouvelle approche, sans avoir plusieurs blocs try-catch.

import to from './to';

async foo(id: string): Promise<A> {
    let err, result;
    [err, result] = await to(someAsyncPromise()); // notice the to() here
    if (err) {
        return 400;
    }
    return 200;
}

D'où la fonction to.ts doit être importée:

export default function to(promise: Promise<any>): Promise<any> {
    return promise.then(data => {
        return [null, data];
    }).catch(err => [err]);
}

Les crédits vont à Dima Grossman dans le lien suivant .

Pedro Lourenço
la source
1
J'utilise cette construction presque exclusivement (beaucoup plus propre) et il y a un module «to» qui existe depuis un certain temps sur npmjs.com/package/await-to-js . Vous n'avez pas besoin que la déclaration séparée soit placée devant la mission déconstruite. Peut également faire juste let [err]=si seulement la vérification des erreurs.
DKebler
3

Ce n'est pas une réponse à celle de @TJ Crowder. Juste un commentaire répondant au commentaire "Et en fait, si l'exception va être convertie en rejet, je ne sais pas si je suis vraiment gêné s'il s'agit d'une erreur. Mes raisons de ne lancer que l'erreur ne s'appliquent probablement pas. "

si votre code utilise async/ await, il est toujours recommandé de rejeter avec un Errorau lieu de 400:

try {
  await foo('a');
}
catch (e) {
  // you would still want `e` to be an `Error` instead of `400`
}
unional
la source
3

Je sais que c'est une vieille question, mais je suis juste tombé sur le fil et il semble y avoir une confusion entre les erreurs et le rejet qui va à l'encontre (dans de nombreux cas, au moins) des conseils souvent répétés de ne pas utiliser la gestion des exceptions pour traiter les cas prévus. Pour illustrer: si une méthode asynchrone essaie d'authentifier un utilisateur et que l'authentification échoue, c'est un rejet (l'un des deux cas prévus) et non une erreur (par exemple, si l'API d'authentification n'était pas disponible.)

Pour m'assurer que je ne divisais pas seulement les cheveux, j'ai exécuté un test de performance de trois approches différentes, en utilisant ce code:

const iterations = 100000;

function getSwitch() {
  return Math.round(Math.random()) === 1;
}

function doSomething(value) {
  return 'something done to ' + value.toString();
}

let processWithThrow = function () {
  if (getSwitch()) {
    throw new Error('foo');
  }
};

let processWithReturn = function () {
  if (getSwitch()) {
    return new Error('bar');
  } else {
    return {}
  }
};

let processWithCustomObject = function () {
  if (getSwitch()) {
    return {type: 'rejection', message: 'quux'};
  } else {
    return {type: 'usable response', value: 'fnord'};
  }
};

function testTryCatch(limit) {
  for (let i = 0; i < limit; i++) {
    try {
      processWithThrow();
    } catch (e) {
      const dummyValue = doSomething(e);
    }
  }
}

function testReturnError(limit) {
  for (let i = 0; i < limit; i++) {
    const returnValue = processWithReturn();
    if (returnValue instanceof Error) {
      const dummyValue = doSomething(returnValue);
    }
  }
}

function testCustomObject(limit) {
  for (let i = 0; i < limit; i++) {
    const returnValue = processWithCustomObject();
    if (returnValue.type === 'rejection') {
      const dummyValue = doSomething(returnValue);
    }
  }
}

let start, end;
start = new Date();
testTryCatch(iterations);
end = new Date();
const interval_1 = end - start;
start = new Date();
testReturnError(iterations);
end = new Date();
const interval_2 = end - start;
start = new Date();
testCustomObject(iterations);
end = new Date();
const interval_3 = end - start;

console.log(`with try/catch: ${interval_1}ms; with returned Error: ${interval_2}ms; with custom object: ${interval_3}ms`);

Certaines des choses qui s'y trouvent sont incluses en raison de mon incertitude concernant l'interpréteur Javascript (j'aime seulement descendre un trou de lapin à la fois); par exemple, j'ai inclus la doSomethingfonction et assigné son retour à dummyValuepour m'assurer que les blocs conditionnels ne seraient pas optimisés.

Mes résultats étaient:

with try/catch: 507ms; with returned Error: 260ms; with custom object: 5ms

Je sais qu'il existe de nombreux cas où cela ne vaut pas la peine de traquer les petites optimisations, mais dans les systèmes à plus grande échelle, ces choses peuvent faire une grande différence cumulative, et c'est une comparaison assez frappante.

SO ... alors que je pense que l'approche de la réponse acceptée est valable dans les cas où vous vous attendez à devoir gérer des erreurs imprévisibles dans une fonction asynchrone, dans les cas où un rejet signifie simplement "vous allez devoir suivre le plan B (ou C ou D…) "Je pense que ma préférence serait de rejeter l'utilisation d'un objet de réponse personnalisé.

RiqueW
la source
2
N'oubliez pas non plus que vous n'avez pas besoin de vous soucier de la gestion des erreurs imprévues dans une fonction asynchrone si l'appel à cette fonction se trouve dans un bloc try / catch dans la portée englobante car - contrairement à Promises - les fonctions asynchrones propagent leurs erreurs renvoyées vers le englobant la portée, où ils sont traités comme des erreurs locales à cette portée. C'est l'un des principaux avantages de l'async / wait!
RiqueW
Les microbenchmarks sont le diable. Regardez de plus près les chiffres. Vous devez faire quelque chose 1000x pour remarquer une différence de 1 ms ici. Oui, l'ajout de throw / catch désoptimisera la fonction. Mais a) si vous attendez quelque chose d'async, il est probable que plusieurs ordres de grandeur prennent plus de 0,0005 ms à se produire en arrière-plan. b) vous devez le faire 1000x pour faire une différence de 1 ms ici.
Jamie Pate