Obsługa wielu połowów w łańcuchu obietnic

125

Wciąż jestem dość nowy w obietnicach i obecnie używam Bluebird, jednak mam scenariusz, w którym nie jestem do końca pewien, jak najlepiej sobie z tym poradzić.

Na przykład mam łańcuch obietnic w aplikacji ekspresowej, takiej jak ta:

repository.Query(getAccountByIdQuery)
        .catch(function(error){
            res.status(404).send({ error: "No account found with this Id" });
        })
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .catch(function(error) {
            res.status(406).send({ OldPassword: error });
        })
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error){
            console.log(error);
            res.status(500).send({ error: "Unable to change password" });
        });

Więc zachowanie, którego szukam, to:

  • Przechodzi do konta za pomocą identyfikatora
  • Jeśli w tym momencie nastąpi odrzucenie, zbombarduj i zwróć błąd
  • Jeśli nie ma błędu, przekształć dokument zwrócony do modelu
  • Sprawdź hasło w dokumencie bazy danych
  • Jeśli hasła nie pasują, zbombarduj i zwróć inny błąd
  • Jeśli nie ma błędu, zmień hasła
  • Następnie powróć sukces
  • Jeśli coś poszło nie tak, zwróć 500

Więc obecnie zaczepy nie wydają się zatrzymywać łańcucha, a to ma sens, więc zastanawiam się, czy jest sposób, abym w jakiś sposób zmusił łańcuch do zatrzymania się w określonym punkcie na podstawie błędów, czy jest lepszy sposób Skonstruować to tak, aby uzyskać jakąś formę zachowania rozgałęzień, jak w przypadku if X do Y else Z.

Każda pomoc byłaby świetna.

Grofit
źródło
Czy możesz wrzucić ponownie lub wrócić wcześniej?
Pieter21

Odpowiedzi:

126

To zachowanie jest dokładnie jak rzut synchroniczny:

try{
    throw new Error();
} catch(e){
    // handle
} 
// this code will run, since you recovered from the error!

To połowa .catch- aby móc naprawić błędy. Może być pożądane ponowne zgłoszenie, aby zasygnalizować, że stan nadal jest błędem:

try{
    throw new Error();
} catch(e){
    // handle
    throw e; // or a wrapper over e so we know it wasn't handled
} 
// this code will not run

Jednak to samo nie zadziała w twoim przypadku, ponieważ błąd zostanie przechwycony przez późniejszą procedurę obsługi. Prawdziwym problemem jest to, że uogólnione procedury obsługi błędów „OBSŁUGUJ WSZYSTKO” są ogólnie złą praktyką i są niezmiernie źle widziane w innych językach programowania i ekosystemach. Z tego powodu Bluebird oferuje połowy typowane i predykatowe.

Dodatkową zaletą jest to, że logika biznesowa w ogóle nie musi (i nie powinna) być świadoma cyklu żądania / odpowiedzi. Zapytanie nie jest odpowiedzialne za decydowanie o stanie HTTP i błędzie klienta, a później, w miarę rozwoju aplikacji, możesz chcieć oddzielić logikę biznesową (jak zapytać o bazę danych i jak przetwarzać dane) od tego, co wysyłasz do klienta (jaki kod statusu http, jaki tekst i jaka odpowiedź).

Oto jak napisałbym twój kod.

Najpierw .Querywyrzuciłbym NoSuchAccountErrorpodklasę, z Promise.OperationalErrorktórej Bluebird już dostarcza. Jeśli nie jesteś pewien, jak podklasować błąd, daj mi znać.

Dodatkowo podklasowałbym to dla, AuthenticationErrora następnie zrobiłbym coś takiego:

function changePassword(queryDataEtc){ 
    return repository.Query(getAccountByIdQuery)
                     .then(convertDocumentToModel)
                     .then(verifyOldPassword)
                     .then(changePassword);
}

Jak widać - jest bardzo przejrzysty i możesz czytać tekst jak instrukcję obsługi tego, co dzieje się w procesie. Jest również oddzielony od żądania / odpowiedzi.

Teraz nazwałbym to z obsługi trasy jako takiego:

 changePassword(params)
 .catch(NoSuchAccountError, function(e){
     res.status(404).send({ error: "No account found with this Id" });
 }).catch(AuthenticationError, function(e){
     res.status(406).send({ OldPassword: error });
 }).error(function(e){ // catches any remaining operational errors
     res.status(500).send({ error: "Unable to change password" });
 }).catch(function(e){
     res.status(500).send({ error: "Unknown internal server error" });
 });

W ten sposób cała logika jest w jednym miejscu, a decyzja, jak obsłużyć błędy klienta, jest w jednym miejscu i nie zaśmiecają się nawzajem.

Benjamin Gruenbaum
źródło
11
Możesz chcieć dodać, że powodem posiadania pośredniej .catch(someSpecificError)procedury obsługi dla określonego błędu jest chęć wyłapania określonego typu błędu (który jest nieszkodliwy), radzenie sobie z nim i kontynuowanie następującego przepływu. Na przykład mam kod startowy zawierający sekwencję czynności do wykonania. Pierwszą rzeczą jest odczytanie pliku konfiguracyjnego z dysku, ale jeśli brakuje tego pliku konfiguracyjnego, jest to błąd OK (program ma wbudowane ustawienia domyślne), więc mogę obsłużyć ten konkretny błąd i kontynuować resztę przepływu. Może być też lepiej, aby nie wychodzić na później.
jfriend00,
1
Pomyślałem, że „To połowa celu .catch - aby móc odzyskać sprawność po błędach” wyjaśniło to jasno, ale dzięki za dalsze wyjaśnienia to dobry przykład.
Benjamin Gruenbaum
1
A co jeśli Bluebird nie jest używany? Zwykłe obietnice es6 mają tylko komunikat o błędzie łańcucha, który jest przekazywany do przechwytywania.
zegarmistrz
3
@clocksmith z ES6 obiecuje, że utknąłeś, łapiąc wszystko i robiąc instanceofręcznie.
Benjamin Gruenbaum,
1
Dla tych, którzy szukają odniesienia do podklas obiektów Error, przeczytaj bluebirdjs.com/docs/api/catch.html#filtered-catch . Artykuł również w dużym stopniu odtwarza podaną tutaj odpowiedź dotyczącą wielu chwytów.
mummybot
47

.catchdziała jak try-catchinstrukcja, co oznacza, że ​​na końcu potrzebujesz tylko jednego haczyka:

repository.Query(getAccountByIdQuery)
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error) {
            if (/*see if error is not found error*/) {
                res.status(404).send({ error: "No account found with this Id" });
            } else if (/*see if error is verification error*/) {
                res.status(406).send({ OldPassword: error });
            } else {
                console.log(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        });
Esailija
źródło
1
Tak, wiedziałem o tym, ale nie chciałem robić ogromnego łańcucha błędów i wydawało się, że jest to bardziej czytelne, gdy było to potrzebne. Stąd haczyk na końcu, ale podoba mi się idea błędów maszynowych, ponieważ jest ona bardziej opisowa co do intencji.
Grofit
8
@Grofit za to, co warto - połowy wpisane w Bluebird były pomysłem Petki (Esailiji) :) Nie trzeba go przekonywać, że są tutaj preferowanym podejściem. Myślę, że nie chciał cię zmylić, ponieważ wiele osób w JS nie jest zbyt świadomych tej koncepcji.
Benjamin Gruenbaum
17

Zastanawiam się, czy istnieje sposób, aby w jakiś sposób zmusić łańcuch do zatrzymania się w pewnym punkcie na podstawie błędów

Nie. Tak naprawdę nie można „zakończyć” łańcucha, chyba że wyrzuci się wyjątek, który powoduje bąbelki do końca. Zobacz odpowiedź Benjamina Gruenbauma, jak to zrobić.

Wyprowadzenie jego wzorca nie polegałoby na rozróżnieniu typów błędów, ale na wykorzystaniu błędów, które mają statusCodei bodypól, które mogą być wysłane z pojedynczego, ogólnego programu .catchobsługi. W zależności od struktury aplikacji jego rozwiązanie może być jednak czystsze.

lub jeśli istnieje lepszy sposób na ustrukturyzowanie tego, aby uzyskać jakąś formę zachowania rozgałęzień

Tak, możesz rozgałęziać się z obietnicami . Oznacza to jednak opuszczenie łańcucha i „powrót” do zagnieżdżania - tak jak w przypadku zagnieżdżonej instrukcji if-else lub try-catch:

repository.Query(getAccountByIdQuery)
.then(function(account) {
    return convertDocumentToModel(account)
    .then(verifyOldPassword)
    .then(function(verification) {
        return changePassword(verification)
        .then(function() {
            res.status(200).send();
        })
    }, function(verificationError) {
        res.status(406).send({ OldPassword: error });
    })
}, function(accountError){
    res.status(404).send({ error: "No account found with this Id" });
})
.catch(function(error){
    console.log(error);
    res.status(500).send({ error: "Unable to change password" });
});
Bergi
źródło
5

Robiłem w ten sposób:

W końcu zostawiasz swój połów. I po prostu wyrzuć błąd, gdy nastąpi to w połowie twojego łańcucha.

    repository.Query(getAccountByIdQuery)
    .then((resultOfQuery) => convertDocumentToModel(resultOfQuery)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')
    .then((model) => verifyOldPassword(model)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')        
    .then(changePassword)
    .then(function(){
        res.status(200).send();
    })
    .catch((error) => {
    if (error.name === 'no_account'){
        res.status(404).send({ error: "No account found with this Id" });

    } else  if (error.name === 'wrong_old_password'){
        res.status(406).send({ OldPassword: error });

    } else {
         res.status(500).send({ error: "Unable to change password" });

    }
});

Twoje inne funkcje prawdopodobnie wyglądałyby mniej więcej tak:

function convertDocumentToModel(resultOfQuery) {
    if (!resultOfQuery){
        throw new Error('no_account');
    } else {
    return new Promise(function(resolve) {
        //do stuff then resolve
        resolve(model);
    }                       
}
Leo Leao
źródło
4

Pewnie trochę za późno na imprezę, ale możliwe jest zagnieżdżenie .catchjak na zdjęciu:

Mozilla Developer Network - Korzystanie z obietnic

Edycja: przesłałem to, ponieważ ogólnie zapewnia żądaną funkcjonalność. Jednak w tym konkretnym przypadku tak nie jest. Ponieważ, jak już szczegółowo wyjaśnili inni, .catchma naprawić błąd. Nie możesz na przykład wysłać odpowiedzi do klienta w wielu .catch wywołaniach zwrotnych, ponieważ .catchbez jawności return rozwiązuje to undefinedw tym przypadku, powodując kontynuację .thenwyzwalania, nawet jeśli twój łańcuch nie jest tak naprawdę rozwiązany, potencjalnie powodując .catchwyzwalanie i wysyłanie po kolejna odpowiedź do klienta, powodująca błąd i prawdopodobnie rzucająca UnhandledPromiseRejectionsię w oczy. Mam nadzieję, że to zawiłe zdanie miało dla ciebie jakiś sens.

denkquer
źródło
1
@AntonMenshov Masz rację. Rozszerzyłem moją odpowiedź, wyjaśniając, dlaczego jego pożądane zachowanie jest nadal niemożliwe przy zagnieżdżaniu
denkquer.
2

Zamiast tego .then().catch()...możesz zrobić .then(resolveFunc, rejectFunc). Ten łańcuch obietnic byłby lepszy, gdybyś załatwił sprawy po drodze. Oto jak bym to przepisał:

repository.Query(getAccountByIdQuery)
    .then(
        convertDocumentToModel,
        () => {
            res.status(404).send({ error: "No account found with this Id" });
            return Promise.reject(null)
        }
    )
    .then(
        verifyOldPassword,
        () => Promise.reject(null)
    )
    .then(
        changePassword,
        (error) => {
            if (error != null) {
                res.status(406).send({ OldPassword: error });
            }
            return Promise.Promise.reject(null);
        }
    )
    .then(
        _ => res.status(200).send(),
        error => {
            if (error != null) {
                console.error(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        }
    );

Uwaga:if (error != null) jest nieco hack do interakcji z ostatniego błędu.

mvndaai
źródło
1

Myślę, że powyższa odpowiedź Benjamina Gruenbauma jest najlepszym rozwiązaniem dla złożonej sekwencji logicznej, ale oto moja alternatywa dla prostszych sytuacji. Po prostu używam errorEncounteredflagi wraz z, return Promise.reject()aby pominąć jakiekolwiek następne thenlub catchoświadczenia. Więc wyglądałoby to tak:

let errorEncountered = false;
someCall({
  /* do stuff */
})
.catch({
  /* handle error from someCall*/
  errorEncountered = true;
  return Promise.reject();
})
.then({
  /* do other stuff */
  /* this is skipped if the preceding catch was triggered, due to Promise.reject */
})
.catch({
  if (errorEncountered) {
    return;
  }
  /* handle error from preceding then, if it was executed */
  /* if the preceding catch was executed, this is skipped due to the errorEncountered flag */
});

Jeśli masz więcej niż dwie pary następnie / catch, prawdopodobnie powinieneś użyć rozwiązania Benjamina Gruenbauma. Ale to działa w przypadku prostej konfiguracji.

Zwróć uwagę, że wersja ostateczna catchma tylko return;zamiast return Promise.reject();, ponieważ nie ma kolejnych, thenktóre powinniśmy pominąć, i liczyłoby się to jako nieobsłużone odrzucenie obietnicy, czego Node nie lubi. Jak napisano powyżej, finał catchzwróci pokojowo rozwiązaną obietnicę.

nazwa_użytkownika_tymczasowego
źródło