Le refus d'une promesse est-il uniquement pour les cas d'erreur?


25

Disons que j'ai cette fonction d'authentifier qui renvoie une promesse. La promesse se résout alors avec le résultat. Faux et vrais sont des résultats attendus, à mon avis, et les rejets ne devraient se produire que dans un cas d'erreur. Ou, un échec de l'authentification est-il considéré comme quelque chose pour lequel vous rejetteriez une promesse?


Si l' authentification échoue, vous devriez rejectet ne retournez pas faux, mais si vous attendez la valeur d'être Bool, alors vous étiez avec succès et vous devez résoudre avec le Bool quelle que soit la valeur. Les promesses sont en quelque sorte des procurations pour les valeurs - elles stockent la valeur renvoyée, donc seulement si la valeur n'a pas pu être obtenue reject. Sinon, vous devriez resolve.

C'est une bonne question. Il touche à l'un des échecs de la conception de la promesse. Il existe deux types d'erreurs, les échecs attendus, par exemple lorsqu'un utilisateur fournit une entrée incorrecte (comme l'échec de la connexion) et les échecs inattendus, qui sont des bogues dans le code. La conception prometteuse fusionne les deux concepts en un seul flux, ce qui rend difficile de distinguer les deux pour la manipulation.
zzzzBov

1
Je dirais que résoudre signifie utiliser la réponse et poursuivre votre application, tandis que rejeter signifie annuler l'opération en cours (et éventuellement réessayer ou faire autre chose).

4
une autre façon de penser - s'il s'agissait d'un appel de méthode synchrone, traiteriez-vous l'échec d'authentification régulier (mauvais nom d'utilisateur / mot de passe) comme un retour falseou une levée d'exception?
wrschneider

2
L' API Fetch en est un bon exemple. Il se déclenche toujours thenlorsque le serveur répond - même si un code d'erreur est renvoyé - et vous devez vérifier le response.ok. Le catchgestionnaire n'est déclenché que pour les erreurs inattendues .
CodingIntrigue

Réponses:


22

Bonne question! Il n'y a pas de réponse difficile. Cela dépend de ce que vous considérez comme exceptionnel à ce point précis du flux .

Rejeter un Promiseest le même que lever une exception. Tous les résultats indésirables ne sont pas exceptionnels , le résultat d' erreurs . Vous pouvez plaider votre cause dans les deux sens:

  1. Échec de l' authentification devrait rejectlePromise , parce que l'appelant attend un Userobjet en retour, et tout est bien une exception à ce flux.

  2. L'authentification échouée devrait resolvele Promise, bien que null, puisque fournir les informations d'identification incorrectes n'est pas vraiment un cas exceptionnel , et l'appelant ne devrait pas s'attendre à ce que le flux entraîne toujours un User.

Notez que je regarde le problème du côté de l'appelant . Dans le flux d'informations, l'appelant s'attend- il à ce que ses actions entraînent une User(et toute autre chose est une erreur), ou est-il logique que cet appelant particulier gère d'autres résultats?

Dans un système multicouche, la réponse peut changer à mesure que les données circulent à travers les couches. Par exemple:

  • La couche HTTP indique RESOLVE! La demande a été envoyée, le socket s'est fermé proprement et le serveur a émis une réponse valide. L' API Fetch fait cela.
  • La couche de protocole indique alors REJETER! Le code d'état dans la réponse était 401, ce qui est correct pour HTTP, mais pas pour le protocole!
  • La couche d'authentification dit NON, RÉSOLVEZ! Il intercepte l'erreur, car 401 est l'état attendu pour un mot de passe incorrect et se résout en unnull utilisateur.
  • Le contrôleur d'interface dit AUCUN DE CELA, REJETTEZ! L'affichage modal à l'écran attendait un nom d'utilisateur et un avatar, et tout ce qui n'est pas cette information est une erreur à ce stade.

Cet exemple en 4 points est évidemment compliqué, mais il illustre 2 points:

  1. Que quelque chose soit une exception / rejet ou non dépend du flux environnant et des attentes
  2. Différentes couches de votre programme peuvent traiter le même résultat différemment, car elles sont situées à différents stades du flux

Encore une fois, pas de réponse difficile. Il est temps de penser et de concevoir!


6

Les promesses ont donc une belle propriété qu'elles apportent JS à partir des langages fonctionnels, c'est-à-dire qu'elles implémentent effectivement ce Eitherconstructeur de type qui colle deux autres types, le Lefttype et le Righttype, en forçant la logique à prendre l'une ou l'autre branche branche.

data Either x y = Left x | Right y

Maintenant, vous remarquez en effet que le type sur le côté gauche est ambigu pour les promesses; vous pouvez rejeter avec n'importe quoi. Cela est vrai car JS est faiblement typé, mais vous voulez être prudent si vous programmez de manière défensive.

La raison en est que JS prendra les throwinstructions du code de gestion des promesses et les regroupera également dans le Leftcôté. Techniquement, dans JS, vous pouvez throwtout, y compris true / false ou une chaîne ou un nombre: mais le code JavaScript jette également des choses sansthrow (lorsque vous faites des choses comme essayer d'accéder aux propriétés sur null) et il y a une API définie pour cela (l' Errorobjet) . Ainsi, lorsque vous vous apprêtez à attraper, il est généralement agréable de pouvoir supposer que ces erreurs sont des Errorobjets. Et comme la rejectpromesse s'agglomérera dans toutes les erreurs de l'un des bogues ci-dessus, vous ne voulez généralement que d' throwautres erreurs, pour que votrecatch déclaration ait une logique simple et cohérente.

Par conséquent, bien que vous puissiez mettre un if-conditionnel dans votre catchet rechercher de fausses erreurs, auquel cas le cas de vérité est trivial,

Either (Either Error ()) ()

vous préférerez probablement la structure logique, au moins pour ce qui sort immédiatement de l'authentificateur, d'un booléen plus simple:

Either Error Bool

En fait, le prochain niveau de logique d'authentification consiste probablement à renvoyer une sorte d' Userobjet contenant l'utilisateur authentifié, de sorte que cela devienne:

Either Error (Maybe User)

et c'est plus ou moins ce à quoi je m'attendrais: retour nulldans le cas où l'utilisateur n'est pas défini, sinon retour {user_id: <number>, permission_to_launch_missiles: <boolean>}. Je m'attendrais à ce que le cas général de ne pas être connecté soit récupérable, par exemple si nous sommes dans une sorte de mode "démo pour les nouveaux clients", et ne devrait pas être mélangé avec des bogues où j'ai accidentellement appelé object.doStuff()quand object.doStuffétaitundefined .

Maintenant que cela est dit, ce que vous voudrez peut-être faire est de définir une exception NotLoggedInou PermissionErrorqui dérive de Error. Ensuite, dans les choses qui en ont vraiment besoin, vous voulez écrire:

function launchMissiles() {
    function actuallyLaunchThem() {
        // stub
    }
    return getAuth().then(auth => {
        if (auth === null) {
            throw new PermissionError('Cannot launch missiles without permission, cannot have permission if not logged in.');
        } else if (auth.permission_to_launch_missiles) {
            return actuallyLaunchThem();
        } else {
            throw new PermissionError(`User ${auth.user_id} does not have permission to launch the missiles.`);
        }
    });
}

3

les erreurs

Parlons d'erreurs.

Il existe deux types d'erreurs:

  • erreurs attendues
  • erreurs inattendues
  • erreurs ponctuelles

Erreurs attendues

Les erreurs attendues sont des états où la mauvaise chose se produit, mais vous savez que cela peut arriver, vous devez donc y faire face.

Ce sont des choses comme l'entrée d'utilisateur ou les demandes du serveur. Vous savez que l'utilisateur peut faire une erreur ou que le serveur est en panne, vous écrivez donc du code de vérification pour vous assurer que le programme demande à nouveau une entrée, ou affiche un message, ou tout autre comportement approprié.

Ceux-ci sont récupérables lorsqu'ils sont manipulés. S'ils ne sont pas manipulés, ils deviennent des erreurs inattendues.

Erreurs inattendues

Les erreurs inattendues (bogues) sont des états où la mauvaise chose se produit parce que le code est incorrect. Vous savez qu'ils finiront par se produire, mais il n'y a aucun moyen de savoir où ou comment les traiter car, par définition, ils sont inattendus.

Ce sont des choses comme la syntaxe et les erreurs logiques. Vous pouvez avoir une faute de frappe dans votre code, vous avez peut-être appelé une fonction avec les mauvais paramètres. Ceux-ci ne sont généralement pas récupérables.

try..catch

Parlons de try..catch .

En JavaScript, thrown'est pas couramment utilisé. Si vous regardez des exemples dans le code, ils seront peu nombreux et généralement structurés dans le sens de

function example(param) {
  if (!Array.isArray(param) {
    throw new TypeError('"param" should be an array!');
  }
  ...
}

À cause de ce, try..catch blocs ne sont pas tous communs non plus pour le flux de contrôle. Il est généralement assez facile d'ajouter quelques vérifications avant d'appeler des méthodes pour éviter les erreurs attendues.

Les environnements JavaScript sont également assez indulgents, donc les erreurs inattendues sont souvent laissées non détectées également.

try..catchne doit pas être rare. Il existe de bons cas d'utilisation, qui sont plus courants dans des langages tels que Java et C #. Java et C # ont l'avantage de tapercatch constructions , de sorte que vous pouvez faire la différence entre les erreurs attendues et inattendues:

C # :
try
{
  var example = DoSomething();
}
catch (ExpectedException e)
{
  DoSomethingElse(e);
}

Cet exemple permet à d'autres exceptions inattendues de remonter et d'être gérées ailleurs (par exemple en étant connecté et en fermant le programme).

En JavaScript, cette construction peut être répliquée via:

try {
  let example = doSomething();
} catch (e) {
  if (e instanceOf ExpectedError) {
    DoSomethingElse(e);
  } else {
    throw e;
  }
}

Pas aussi élégant, ce qui explique pourquoi c'est rare.

Les fonctions

Parlons des fonctions.

Si vous utilisez le principe de responsabilité unique , chaque classe et chaque fonction doivent avoir un objectif unique.

Par exemple, authenticate()peut authentifier un utilisateur.

Cela pourrait être écrit comme suit:

const user = authenticate();
if (user == null) {
  // keep doing stuff
} else {
  // handle expected error
}

Alternativement, il peut être écrit comme suit:

try {
  const user = authenticate();
  // keep doing stuff
} catch (e) {
  if (e instanceOf AuthenticationError) {
    // handle expected error
  } else {
    throw e;
  }
}

Les deux sont acceptables.

Promesses

Parlons de promesses.

Les promesses sont une forme asynchrone de try..catch. Appelez new Promiseou Promise.resolvecommencez votre trycode. Appelant throwou Promise.rejectvous envoie le catchcode.

Promise.resolve(value)   // try
  .then(doSomething)     // try
  .then(doSomethingElse) // try
  .catch(handleError)    // catch

Si vous avez une fonction asynchrone pour authentifier un utilisateur, vous pouvez l'écrire comme:

authenticate()
  .then((user) => {
    if (user == null) {
      // keep doing stuff
    } else {
      // handle expected error
    }
  });

Alternativement, il peut être écrit comme suit:

authenticate()
  .then((user) => {
    // keep doing stuff
  })
  .catch((e) => {
    if (e instanceOf AuthenticationError) {
      // handle expected error
    } else {
      throw e;
    }
  });

Les deux sont acceptables.

Imbrication

Parlons de l'imbrication.

try..catchpeut être imbriqué. Votre authenticate()méthode peut avoir en interne un try..catchbloc tel que:

try {
  const credentials = requestCredentialsFromUser();
  const user = getUserFromServer(credentials);
} catch (e) {
  if (e instanceOf CredentialsError) {
    // handle failure to request credentials
  } else if (e instanceOf ServerError) {
    // handle failure to get data from server
  } else {
    throw e; // no idea what happened
  }
}

De même, les promesses peuvent être imbriquées. Votre authenticate()méthode asynchrone peut utiliser en interne des promesses:

requestCredentialsFromUser()
  .then(getUserFromServer)
  .catch((e) => {
    if (e instanceOf CredentialsError) {
      // handle failure to request credentials
    } else if (e instanceOf ServerError) {
      // handle failure to get data from server
    } else {
      throw e; // no idea what happened
    }
  });

Alors, quelle est la réponse?

Ok, je pense qu'il est temps pour moi de répondre à la question:

Un échec de l'authentification est-il considéré comme une chose pour laquelle vous rejetteriez une promesse?

La réponse la plus simple que je puisse donner est que vous devriez rejeter une promesse partout où vous voudriez autrement throw une exception s'il s'agissait d'un code synchrone.

Si votre flux de contrôle est plus simple en ayant quelques ifvérifications dans vos thenrelevés, il n'est pas nécessaire de rejeter une promesse.

Si votre flux de contrôle est plus simple en rejetant une promesse puis en vérifiant les types d'erreurs dans votre code de gestion des erreurs, faites-le à la place.


0

J'ai utilisé la branche "rejeter" d'une promesse pour représenter l'action "annuler" des boîtes de dialogue de l'interface utilisateur jQuery. Cela semblait plus naturel que d'utiliser la branche "résoudre", notamment parce qu'il y a souvent plusieurs options "fermer" dans une boîte de dialogue.


La plupart des puristes que je connais seraient en désaccord avec vous.

0

Gérer une promesse est plus ou moins comme une condition «si». C'est à vous de décider si vous voulez "résoudre" ou "rejeter" si l'authentification a échoué.


1
la promesse est asynchrone try..catch, non if.
zzzzBov

@zzzBox donc selon cette logique, vous devez utiliser une promesse comme asynchrone try...catchet simplement dire que si vous avez pu terminer et obtenir un résultat, vous devez résoudre quelle que soit la valeur reçue, sinon vous devriez rejeter?

@somethinghere, non, vous avez mal interprété mon argument. try { if (!doSomething()) throw whatever; doSomethingElse() } catch { ... }est parfaitement bien, mais la construction que Promisereprésente un est la try..catchpartie, pas la ifpartie.
zzzzBov

@zzzzBov J'ai eu ça en toute honnêteté :) J'aime l'analogie. Mais ma logique est simplement que si doSomething()échoue, alors il lancera, mais sinon il pourrait contenir la valeur dont vous avez besoin (votre ifci-dessus est légèrement déroutant car cela ne fait pas partie de votre idée ici :)). Vous ne devez rejeter que s'il y a une raison de lancer (dans l'analogie), donc si le test a échoué. Si le test a réussi, vous devez toujours le résoudre, que sa valeur soit positive, non?

@somethinghere, j'ai décidé d'écrire une réponse (en supposant que cela reste ouvert assez longtemps), car les commentaires ne suffisent pas à exprimer mes pensées.
zzzzBov
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.