Wird ein Versprechen nur für Fehlerfälle abgelehnt?

25

Nehmen wir an, ich lasse diese Funktion authentifizieren, die ein Versprechen zurückgibt. Das Versprechen löst sich dann mit dem Ergebnis auf. Falsch und wahr sind erwartete Ergebnisse, wie ich es sehe, und Ablehnungen sollten nur in einem Fehlerfall auftreten. Oder wird ein Authentifizierungsfehler als etwas angesehen, für das Sie ein Versprechen ablehnen würden?

Mathieu Bertin
quelle
Wenn die Authentifizierung fehlgeschlagen ist , sollten Sie rejectund nicht falsch zurück, aber wenn Sie den Wert erwarten eine sein Bool, dann Sie waren erfolgreich , und Sie sollten mit dem Bool unabhängig vom Wert lösen. Versprechungen sind eine Art Stellvertreter für Werte - sie speichern den zurückgegebenen Wert, also sollten Sie nur dann, wenn der Wert nicht erhalten werden konnte reject. Ansonsten solltest du resolve.
Das ist eine gute Frage. Es berührt einen der Fehler des Versprechenentwurfs. Es gibt zwei Arten von Fehlern: erwartete Fehler, z. B. wenn ein Benutzer falsche Eingaben macht (z. B. wenn er sich nicht anmeldet), und unerwartete Fehler, die Fehler im Code sind. Das Versprechen-Design vereint die beiden Konzepte in einem einzigen Ablauf, wodurch es schwierig wird, die beiden für die Handhabung zu unterscheiden.
zzzzBov
1
Ich würde sagen, dass das Auflösen das Verwenden der Antwort und das Fortsetzen Ihrer Anwendung bedeutet, während das Zurückweisen das Abbrechen des aktuellen Vorgangs (und möglicherweise einen erneuten Versuch oder eine andere Aktion) bedeutet.
4
Eine andere Möglichkeit, darüber nachzudenken: Wenn dies ein synchroner Methodenaufruf wäre, würden Sie einen regelmäßigen Authentifizierungsfehler (falscher Benutzername / falsches Kennwort) als Rückgabe falseoder Auslösen einer Ausnahme behandeln?
Wrschneider
2
Die Fetch-API ist ein gutes Beispiel dafür. Es wird immer ausgelöst, thenwenn der Server antwortet - auch wenn ein Fehlercode zurückgegeben wird - und Sie müssen das überprüfen response.ok. Der catchHandler wird nur bei unerwarteten Fehlern ausgelöst .
CodingIntrigue

Antworten:

22

Gute Frage! Es gibt keine harte Antwort. Dies hängt davon ab, was Sie an diesem bestimmten Punkt des Flusses für außergewöhnlich halten .

A zurückzuweisen Promiseist dasselbe wie eine Ausnahme auszulösen . Nicht alle unerwünschten Ergebnisse sind außergewöhnlich , die Folge von Fehlern . Sie könnten Ihren Fall in beide Richtungen argumentieren:

  1. Fehlgeschlagene Authentifizierung sollte rejectdas Promise, weil der Anrufer ein erwartetes UserObjekt im Gegenzug, und alles anderes ist eine Ausnahme von dieser Strömung.

  2. Fehlgeschlagene Authentifizierung sollte resolvedas Promise, wenn auch zu null, da die falschen Zugangsdaten einzugeben nicht wirklich eine ist Ausnahmefall, und der Anrufer sollte der Fluss immer ergibt eine nicht erwarten User.

Beachten Sie, dass ich das Problem von der Seite des Anrufers aus betrachte . Erwartet der Anrufer im Informationsfluss, dass seine Aktionen zu Usereinem Fehler führen (und alles andere ist ein Fehler), oder ist es sinnvoll, dass dieser bestimmte Anrufer mit anderen Ergebnissen umgeht?

In einem mehrschichtigen System kann sich die Antwort ändern, wenn die Daten durch die Schichten fließen. Beispielsweise:

  • HTTP-Ebene sagt RESOLVE! Die Anforderung wurde gesendet, der Socket ordnungsgemäß geschlossen und der Server hat eine gültige Antwort gesendet. Die Fetch-API erledigt dies.
  • Protokollschicht sagt dann REJECT! Der Statuscode in der Antwort war 401, was für HTTP in Ordnung ist, aber nicht für das Protokoll!
  • Authentifizierungsschicht sagt NEIN, AUFLÖSEN! Der Fehler wird abgefangen, da 401 der erwartete Status für ein falsches Kennwort ist und in einen nullBenutzer aufgelöst wird.
  • Schnittstellencontroller sagt KEINES, ABLEHNEN! Das auf dem Bildschirm angezeigte Modal erwartete einen Benutzernamen und einen Avatar, und alles andere als diese Informationen ist an dieser Stelle ein Fehler.

Dieses 4-Punkte-Beispiel ist offensichtlich kompliziert, zeigt aber 2 Punkte:

  1. Ob etwas eine Ausnahme / Ablehnung ist oder nicht, hängt von der Strömung und den Erwartungen ab
  2. Verschiedene Ebenen Ihres Programms können dasselbe Ergebnis unterschiedlich behandeln, da sie sich in verschiedenen Phasen des Ablaufs befinden

Also nochmal, keine harte Antwort. Zeit zum Nachdenken und Gestalten!

slezica
quelle
6

Promises haben also eine nette Eigenschaft, dass sie JS aus funktionalen Sprachen holen. Das heißt, sie implementieren tatsächlich diesen EitherTypkonstruktor, der zwei andere Typen, den LeftTyp und den RightTyp, zusammenhält, indem sie die Logik zwingen, entweder den einen oder den anderen Zweig zu nehmen Ast.

data Either x y = Left x | Right y

Jetzt bemerken Sie tatsächlich, dass der Typ auf der linken Seite für Versprechungen mehrdeutig ist; Sie können mit allem ablehnen. Dies ist wahr, weil JS schwach getippt ist, aber Sie sollten vorsichtig sein, wenn Sie defensiv programmieren.

Der Grund dafür ist, dass JS throwAnweisungen aus dem Code zum Umgang mit Versprechungen übernimmt und sie auch in diesen Code einbindet Left. Technisch gesehen können Sie in JS throwalles, einschließlich true / false oder eine Zeichenfolge oder eine Zahl, aber JavaScript-Code wirft auch Dinge ohnethrow (wenn Sie versuchen, auf Eigenschaften für Nullen zuzugreifen), und es gibt eine festgelegte API für diese (das ErrorObjekt). . Wenn Sie sich also dem Fangen nähern, ist es normalerweise schön anzunehmen, dass diese Fehler ErrorObjekte sind. Und da sich die rejectfür das Versprechen bei Fehlern aus einem der oben genannten Fehler zusammenballen, möchten Sie im Allgemeinen nur throwandere Fehler, damit Ihre catchAussage eine einfache, konsistente Logik hat.

Deshalb , obwohl Sie können eine if-Bedingung in Ihrem setzen catchund sucht falsche Fehler, in diesem Fall die Wahrheit Fall trivial ist,

Either (Either Error ()) ()

Sie werden wahrscheinlich die logische Struktur eines einfacheren Booleschen bevorzugen, zumindest für das, was unmittelbar aus dem Authentifikator hervorgeht:

Either Error Bool

Tatsächlich besteht die nächste Ebene der Authentifizierungslogik wahrscheinlich darin, eine Art UserObjekt zurückzugeben, das den authentifizierten Benutzer enthält.

Either Error (Maybe User)

und das ist mehr oder weniger das, was ich erwarten würde: return nullfür den Fall, dass der Benutzer nicht definiert ist, andernfalls return {user_id: <number>, permission_to_launch_missiles: <boolean>}. Ich würde erwarten , dass der allgemeine Fall nicht in salvageable protokolliert ist, zum Beispiel , wenn wir in einer Art „Demo zu neuen Kunden“ -Modus sind, und soll mit Bugs nicht gemischt sein , wo ich zufällig genannt , object.doStuff()wenn object.doStuffwar undefined.

Nachdem dies gesagt wurde, möchten Sie möglicherweise eine NotLoggedInoder eine PermissionErrorAusnahme definieren, die von abgeleitet ist Error. Dann wollen Sie in den Dingen, die es wirklich brauchen, schreiben:

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.`);
        }
    });
}
CR Drost
quelle
3

Fehler

Sprechen wir über Fehler.

Es gibt zwei Arten von Fehlern:

  • erwartete Fehler
  • unerwartete Fehler
  • Off-by-One-Fehler

Erwartete Fehler

Erwartete Fehler sind Zustände, in denen das Falsche passiert, aber Sie wissen, dass dies möglich ist.

Dies sind Dinge wie Benutzereingaben oder Serveranforderungen. Sie wissen, dass der Benutzer möglicherweise einen Fehler gemacht hat oder der Server heruntergefahren ist. Schreiben Sie daher einen Überprüfungscode, um sicherzustellen, dass das Programm erneut nach Eingaben fragt oder eine Meldung anzeigt, oder was auch immer für ein anderes Verhalten angemessen ist.

Diese können bei der Bearbeitung wiederhergestellt werden. Wenn sie unbehandelt bleiben, werden sie zu unerwarteten Fehlern.

Unerwartete Fehler

Unerwartete Fehler (Bugs) sind Zustände, in denen das Falsche passiert, weil der Code falsch ist. Sie wissen, dass sie irgendwann eintreten werden, aber es gibt keine Möglichkeit zu wissen, wo und wie Sie mit ihnen umgehen sollen, da sie per Definition unerwartet sind.

Dies sind Dinge wie Syntax- und Logikfehler. Möglicherweise haben Sie einen Tippfehler in Ihrem Code. Möglicherweise haben Sie eine Funktion mit den falschen Parametern aufgerufen. Diese können normalerweise nicht wiederhergestellt werden.

try..catch

Lass uns darüber reden try..catch.

Wird in JavaScript thrownicht häufig verwendet. Wenn Sie sich nach Beispielen im Code umschauen, werden sie selten und in der Regel nach dem Muster von strukturiert sein

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

Aus diesem Grund sind try..catchBlöcke auch für den Steuerungsfluss nicht allzu häufig. Es ist normalerweise ziemlich einfach, einige Prüfungen hinzuzufügen, bevor Methoden aufgerufen werden, um erwartete Fehler zu vermeiden.

JavaScript-Umgebungen sind auch ziemlich nachsichtig, so dass auch unerwartete Fehler oft unbemerkt bleiben.

try..catchmuss nicht ungewöhnlich sein. Es gibt einige nützliche Anwendungsfälle, die in Sprachen wie Java und C # häufiger vorkommen. Java und C # haben den Vorteil typisierter catchKonstrukte, sodass Sie zwischen erwarteten und unerwarteten Fehlern unterscheiden können:

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

In diesem Beispiel können andere unerwartete Ausnahmen auftreten und an anderer Stelle behandelt werden (z. B. durch Protokollieren und Schließen des Programms).

In JavaScript kann dieses Konstrukt repliziert werden über:

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

Nicht so elegant, was ein Grund dafür ist, dass es ungewöhnlich ist.

Funktionen

Sprechen wir über Funktionen.

Wenn Sie das Prinzip der Einzelverantwortung anwenden , sollte jede Klasse und Funktion einem bestimmten Zweck dienen.

Zum Beispiel authenticate()könnte ein Benutzer authentifizieren.

Dies könnte geschrieben werden als:

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

Alternativ könnte es geschrieben werden als:

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

Beides ist akzeptabel.

Versprechen

Sprechen wir über Versprechen.

Versprechen sind eine asynchrone Form von try..catch. Aufruf new Promiseoder Promise.resolveStart Ihres tryCodes. Rufen Sie an throwoder Promise.rejectschickt Sie an den catchCode.

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

Wenn Sie eine asynchrone Funktion zur Authentifizierung eines Benutzers haben, können Sie diese folgendermaßen schreiben:

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

Alternativ könnte es geschrieben werden als:

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

Beides ist akzeptabel.

Nisten

Sprechen wir über das Verschachteln.

try..catchkann verschachtelt werden. Ihre authenticate()Methode hat möglicherweise intern einen try..catchBlock wie:

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

Ebenso können Versprechen geschachtelt werden. Ihre asynchrone authenticate()Methode verwendet möglicherweise intern Versprechen:

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

Also, wie lautet die Antwort?

Ok, ich denke, es ist Zeit für mich, die Frage tatsächlich zu beantworten:

Wird ein Authentifizierungsfehler als etwas angesehen, für das Sie ein Versprechen ablehnen würden?

Die einfachste Antwort, die ich geben kann, ist, dass Sie ein Versprechen überall ablehnen sollten, wo Sie sonst throweine Ausnahme wünschen würden, wenn es synchroner Code wäre.

Wenn Ihr Kontrollfluss durch einige ifÜberprüfungen Ihrer thenAussagen einfacher ist , müssen Sie ein Versprechen nicht ablehnen.

Wenn Ihr Kontrollfluss einfacher ist, indem Sie ein Versprechen ablehnen und dann in Ihrem Fehlerbehandlungscode nach Fehlertypen suchen, tun Sie dies stattdessen.

zzzzBov
quelle
0

Ich habe den Zweig "Zurückweisen" eines Versprechens verwendet, um die Aktion "Abbrechen" von Dialogfeldern der jQuery-Benutzeroberfläche darzustellen. Dies erschien natürlicher als die Verwendung des Zweigs "Auflösen", nicht zuletzt, weil in einem Dialogfeld häufig mehrere "Schließen" -Optionen vorhanden sind.

Alnitak
quelle
Die meisten Puristen, die ich kenne, würden Ihnen nicht zustimmen.
0

Die Abwicklung eines Versprechens ist mehr oder weniger eine "wenn" Bedingung. Es liegt an Ihnen, ob Sie das Problem lösen oder ablehnen möchten, wenn die Authentifizierung fehlgeschlagen ist.

evilReiko
quelle
1
Versprechen ist eine asynchrone try..catch, nicht if.
zzzzBov
@zzzBox Nach dieser Logik sollten Sie also ein Versprechen als asynchrones Versprechen verwenden try...catchund einfach sagen, dass Sie unabhängig vom empfangenen Wert eine Lösung finden sollten, wenn Sie ein Ergebnis erzielen konnten. Andernfalls sollten Sie dies ablehnen.
@somethinghere, nein, du hast mein Argument falsch verstanden. try { if (!doSomething()) throw whatever; doSomethingElse() } catch { ... }ist vollkommen in Ordnung, aber das Konstrukt, das a Promisedarstellt, ist der try..catchTeil, nicht der ifTeil.
zzzzBov
@zzzzBov Ich habe das in aller Fairness :) Ich mag die Analogie. Aber meine Logik ist einfach, dass wenn es doSomething()fehlschlägt, es auslöst, aber wenn nicht, könnte es den Wert enthalten, den Sie benötigen (Ihr ifobiges ist etwas verwirrend, da es nicht Teil Ihrer Idee hier ist :)). Sie sollten nur ablehnen, wenn es einen Grund zum Werfen gibt (in der Analogie), also wenn der Test fehlgeschlagen ist. Wenn der Test erfolgreich war, sollten Sie immer entscheiden, ob der Wert positiv ist, oder?
@somethinghere, ich habe beschlossen, eine Antwort zu schreiben (vorausgesetzt, diese bleibt lange genug offen), da Kommentare nicht ausreichen, um meine Gedanken auszudrücken.
zzzzBov