Czy odrzucenie obietnicy dotyczy tylko błędów?

25

Powiedzmy, że mam funkcję uwierzytelnienia, która zwraca obietnicę. Obietnica rozwiązuje się z wynikiem. Fałsz i prawda są oczekiwanymi rezultatami, tak jak ja to widzę, a odrzucenia powinny wystąpić tylko w przypadku błędu. A może niepowodzenie w uwierzytelnianiu to coś, za co odrzucisz obietnicę?

Mathieu Bertin
źródło
Jeśli uwierzytelnianie nie powiodło się, należy rejecti nie zwróci false, ale jeśli spodziewasz wartość być Bool, to były udane i należy rozwiązać z Bool niezależnie od wartości. Obietnice są swego rodzaju proxy dla wartości - przechowują zwróconą wartość, więc tylko wtedy, gdy nie można jej uzyskać reject. W przeciwnym razie powinieneś resolve.
To dobre pytanie. Dotyka jednej z wad obiecanego projektu. Istnieją dwa rodzaje błędów, oczekiwane awarie, na przykład gdy użytkownik wprowadza nieprawidłowe dane wejściowe (takie jak niepowodzenie logowania) i nieoczekiwane awarie, które są błędami w kodzie. Obietnica projektu łączy te dwie koncepcje w jeden przepływ, co utrudnia ich rozróżnienie pod względem obsługi.
zzzzBov,
1
Powiedziałbym, że rozwiązanie oznacza użycie odpowiedzi i kontynuowanie aplikacji, a odrzucenie oznacza anulowanie bieżącej operacji (i ewentualnie spróbuj ponownie lub zrób coś innego).
4
inny sposób myślenia o tym - gdyby to było synchroniczne wywołanie metody, czy traktujesz zwykłe niepowodzenie uwierzytelnienia (zła nazwa użytkownika / hasło) jako powrót falselub zgłoszenie wyjątku?
wrschneider,
2
API Fetch jest dobrym tego przykładem. Zawsze uruchamia się, thengdy serwer odpowiada - nawet jeśli zostanie zwrócony kod błędu - i musisz to sprawdzić response.ok. Program catchobsługi jest uruchamiany tylko w przypadku nieoczekiwanych błędów.
CodingIntrigue

Odpowiedzi:

22

Dobre pytanie! Nie ma trudnej odpowiedzi. To zależy od tego, co uważasz za wyjątkowe w tym konkretnym punkcie przepływu .

Odrzucenie a Promisejest tym samym, co zgłoszenie wyjątku. Nie wszystkie niepożądane wyniki są wyjątkowe , są wynikiem błędów . Możesz argumentować swoją sprawę na dwa sposoby:

  1. Uwierzytelniania nie powinien , ponieważ rozmówca spodziewa się obiektu w zamian, a wszystko inne jest wyjątek od tego przepływu.rejectPromiseUser

  2. Uwierzytelniania nie powinien , aczkolwiek , ponieważ dostarczanie niewłaściwych referencji nie jest naprawdę wyjątkowy przypadek, a dzwoniący nie należy oczekiwać, że przepływ zawsze skutkować .resolvePromisenullUser

Pamiętaj, że patrzę na problem od strony dzwoniącego . Czy w przepływie informacji osoba dzwoniąca oczekuje, że jego działania spowodują błąd User(a cokolwiek innego jest błędem), czy może ma sens, aby ten konkretny rozmówca obsługiwał inne wyniki?

W systemie wielowarstwowym odpowiedź może się zmieniać w miarę przepływu danych przez warstwy. Na przykład:

  • Warstwa HTTP mówi ROZWIĄZANIE! Żądanie zostało wysłane, gniazdo było zamknięte, a serwer wysłał prawidłową odpowiedź. API Fetch to robi.
  • Następnie warstwa protokołu mówi ODRZUĆ! Kod stanu w odpowiedzi to 401, co jest poprawne dla HTTP, ale nie dla protokołu!
  • Warstwa uwierzytelniania mówi NIE, ROZWIĄZANIE! Wyłapuje błąd, ponieważ 401 to oczekiwany stan niepoprawnego hasła i jest rozwiązywany dla nullużytkownika.
  • Kontroler interfejsu mówi ŻADNY Z TYCH, ODRZUĆ! Modal wyświetlany na ekranie oczekiwał nazwy użytkownika i awatara, a wszystko inne niż ta informacja jest w tym momencie błędem.

Ten 4-punktowy przykład jest oczywiście skomplikowany, ale ilustruje 2 punkty:

  1. To, czy coś jest wyjątkiem / odrzuceniem, zależy od otaczającego przepływu i oczekiwań
  2. Różne warstwy programu mogą traktować ten sam wynik inaczej, ponieważ znajdują się na różnych etapach przepływu

Więc znowu, nie ma twardej odpowiedzi. Czas pomyśleć i zaprojektować!

Slezica
źródło
6

Więc obietnice mają fajną właściwość, że przynoszą JS z języków funkcjonalnych, to znaczy, że faktycznie implementują ten Eitherkonstruktor typów, który skleja dwa inne typy, Lefttyp i Righttyp, zmuszając logikę do przyjęcia jednej gałęzi lub drugiej Oddział.

data Either x y = Left x | Right y

Teraz rzeczywiście zauważasz, że typ po lewej stronie jest niejednoznaczny w odniesieniu do obietnic; możesz odrzucić za pomocą wszystkiego. To prawda, ponieważ JS jest słabo wpisany, ale chcesz zachować ostrożność, jeśli programujesz w defensywie.

Powodem jest to, że JS weźmie throwwyciągi z kodu obsługującego obietnice i umieści go również po Leftstronie tego. Technicznie w JS możesz throwwszystko, w tym prawda / fałsz, ciąg znaków lub liczbę: ale kod JavaScript wyrzuca rzeczy bezthrow (gdy robisz takie rzeczy, jak próba uzyskania dostępu do właściwości na wartości null) i istnieje ustalone API dla tego ( Errorobiektu) . Więc kiedy zaczynasz łapać, zazwyczaj dobrze jest założyć, że te błędy są Errorobiektami. A ponieważ rejectobietnica for będzie gromadzić się w dowolnych błędach któregokolwiek z powyższych błędów, generalnie chcesz tylko throwinne błędy, aby twoje catchoświadczenie miało prostą, spójną logikę.

Dlatego chociaż można umieścić if-warunkowy w swoim catchi szukać fałszywych błędów, w tym przypadku sprawa jest banalna prawda,

Either (Either Error ()) ()

prawdopodobnie wolisz strukturę logiczną, przynajmniej dla tego, co pochodzi bezpośrednio z uwierzytelniacza, prostszej wartości logicznej:

Either Error Bool

W rzeczywistości kolejnym poziomem logiki uwierzytelnienia jest prawdopodobnie zwrócenie jakiegoś Userobiektu zawierającego uwierzytelnionego użytkownika, co powoduje , że staje się:

Either Error (Maybe User)

i mniej więcej tego się spodziewałbym: powróć nullw przypadku, gdy użytkownik nie jest zdefiniowany, w przeciwnym razie powróć {user_id: <number>, permission_to_launch_missiles: <boolean>}. Spodziewałbym się, że ogólny przypadek braku zalogowania jest możliwy do odzyskania, na przykład, jeśli jesteśmy w trybie demonstracyjnym dla nowych klientów i nie powinniśmy mieszać się z błędami, do których przypadkowo zadzwoniłem, object.doStuff()kiedy object.doStuffbyłem undefined.

Teraz powiedział, że to, co możesz zrobić, to określić NotLoggedInczy PermissionErrorwyjątek, który wywodzi Error. Następnie w rzeczach, które naprawdę tego potrzebują, chcesz napisać:

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
źródło
3

Błędy

Porozmawiajmy o błędach.

Istnieją dwa rodzaje błędów:

  • oczekiwane błędy
  • nieoczekiwane błędy
  • błędy off-by-one

Oczekiwane błędy

Oczekiwane błędy to stany, w których dzieje się coś złego, ale wiesz, że tak, więc sobie z tym poradzisz.

Są to rzeczy takie jak dane wejściowe użytkownika lub żądania serwera. Wiesz, że użytkownik może popełnić błąd lub serwer może nie działać, więc napisz kod sprawdzający, aby upewnić się, że program ponownie poprosi o wprowadzenie danych, wyświetli komunikat lub cokolwiek innego, co jest odpowiednie.

Można je odzyskać, gdy są obsługiwane. Pozostawione bez obsługi stają się nieoczekiwanymi błędami.

Nieoczekiwane błędy

Nieoczekiwane błędy (błędy) to stany, w których dzieje się coś złego, ponieważ kod jest nieprawidłowy. Wiesz, że w końcu się zdarzą, ale nie ma sposobu, aby wiedzieć, gdzie i jak sobie z nimi poradzić, ponieważ z definicji są nieoczekiwane.

Są to między innymi błędy składniowe i logiczne. Być może masz literówkę w kodzie, mogłeś wywołać funkcję z niewłaściwymi parametrami. Zazwyczaj nie można ich odzyskać.

try..catch

Porozmawiajmy o tym try..catch.

W JavaScript thrownie jest powszechnie używany. Jeśli rozejrzysz się za przykładami w kodzie, to będzie ich niewiele i zwykle są zbudowane według linii

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

Z tego powodu try..catchbloki nie są tak często spotykane w przepływie sterowania. Zazwyczaj dość łatwo jest dodać kilka sprawdzeń przed wywołaniem metod, aby uniknąć oczekiwanych błędów.

Środowiska JavaScript są również dość wybaczające, więc nieoczekiwane błędy również często pozostają niezauważone.

try..catchnie musi być rzadkie. Istnieje kilka fajnych przypadków użycia, które są bardziej powszechne w językach takich jak Java i C #. Java i C # mają tę zaletę catch, że można je rozróżnić między oczekiwanymi i nieoczekiwanymi błędami:

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

Ten przykład pozwala przepłynąć innym nieoczekiwanym wyjątkom i zająć się nimi gdzie indziej (na przykład poprzez zalogowanie i zamknięcie programu).

W JavaScript ta konstrukcja może być replikowana poprzez:

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

Nie tak elegancki, co jest jednym z powodów, dla których jest to rzadkie.

Funkcje

Porozmawiajmy o funkcjach.

Jeśli zastosujesz zasadę pojedynczej odpowiedzialności , każda klasa i funkcja powinna służyć jednemu celowi.

Na przykład authenticate()może uwierzytelnić użytkownika.

Może to być zapisane jako:

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

Alternatywnie można go zapisać jako:

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

Oba są dopuszczalne.

Obietnice

Porozmawiajmy o obietnicach.

Obietnice są formą asynchroniczną try..catch. Dzwonię new Promiselub Promise.resolveuruchamia trykod. Dzwonię throwlub Promise.rejectwysyła cię na catchkod.

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

Jeśli masz funkcję asynchroniczną do uwierzytelnienia użytkownika, możesz zapisać go jako:

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

Alternatywnie można go zapisać jako:

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

Oba są dopuszczalne.

Zagnieżdżanie

Porozmawiajmy o zagnieżdżaniu.

try..catchmożna zagnieżdżać. Twoja authenticate()metoda może mieć try..catchblok wewnętrzny, taki jak:

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

Podobnie obietnice mogą być zagnieżdżone. Twoja authenticate()metoda asynchroniczna może wewnętrznie wykorzystywać obietnice:

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

Więc jaka jest odpowiedź?

Ok, myślę, że nadszedł czas, abym faktycznie odpowiedział na pytanie:

Czy błąd uwierzytelnienia jest czymś, co można odrzucić obietnicą?

Najprostszą odpowiedzią, jaką mogę udzielić, jest to, że powinieneś odrzucić obietnicę w dowolnym miejscu, w którym inaczej byłby throwwyjątek, gdyby był to kod synchroniczny.

Jeśli przepływ kontroli jest prostszy dzięki kilku ifkontrolom w thenwyciągach, nie musisz odrzucać obietnicy.

Jeśli przepływ kontroli jest prostszy, odrzucając obietnicę, a następnie sprawdzając rodzaje błędów w kodzie obsługi błędów, zrób to zamiast tego.

zzzzBov
źródło
0

Użyłem gałęzi „odrzucenia” obietnicy do reprezentowania akcji „anuluj” w oknach dialogowych interfejsu użytkownika jQuery. Wydawało się to bardziej naturalne niż używanie gałęzi „rozwiązuj”, zwłaszcza dlatego, że w oknie dialogowym często znajduje się wiele opcji „zamykania”.

Alnitak
źródło
Większość purystów, których znam, nie zgadza się z tobą.
0

Obsługa obietnicy jest mniej więcej taka, jak warunek „jeśli”. Od Ciebie zależy, czy chcesz „rozwiązać”, czy „odrzucić”, jeśli uwierzytelnienie się nie powiedzie.

evilReiko
źródło
1
Obietnica jest asynchroniczny try..catch, nie if.
zzzzBov,
@zzzBox, więc zgodnie z tą logiką powinieneś używać obietnicy jako asynchronicznej try...catchi po prostu powiedzieć, że jeśli byłeś w stanie wypełnić i uzyskać wynik, powinieneś rozwiązać niezależnie od otrzymanej wartości, w przeciwnym razie powinieneś odrzucić?
@ coś tam, nie, źle zrozumiałeś mój argument. try { if (!doSomething()) throw whatever; doSomethingElse() } catch { ... }jest całkowicie w porządku, ale konstrukcja, którą Promisereprezentuje a, jest try..catchczęścią, a nie ifczęścią.
zzzzBov,
@zzzzBov Mam to uczciwie :) Lubię analogię. Ale moja logika jest taka, że ​​jeśli doSomething()zawiedzie, to rzuci, ale jeśli nie, może zawierać potrzebną wartość (twoje ifpowyższe jest nieco mylące, ponieważ nie jest częścią twojego pomysłu tutaj :)). Powinieneś odrzucić tylko, jeśli istnieje powód do rzucenia (w analogii), więc jeśli test się nie powiódł. Jeśli test się powiedzie, zawsze powinieneś rozwiązać, niezależnie od tego, czy jego wartość jest dodatnia, prawda?
@ coś tam, postanowiłem napisać odpowiedź (zakładając, że pozostaje ona otwarta wystarczająco długo), ponieważ komentarze nie wystarczą do wyrażenia moich myśli.
zzzzBov,