Przerwij łańcuch obietnic i wywołaj funkcję w oparciu o krok w łańcuchu, w którym jest zerwana (odrzucona)

135

Aktualizacja:

Aby pomóc przyszłym widzom tego posta, stworzyłem to demo odpowiedzi Plumy .

Pytanie:

Mój cel wydaje się dość prosty.

  step(1)
  .then(function() {
    return step(2);
  }, function() {
    stepError(1);
    return $q.reject();
  })
  .then(function() {

  }, function() {
    stepError(2);
  });

  function step(n) {
    var deferred = $q.defer();
    //fail on step 1
    (n === 1) ? deferred.reject() : deferred.resolve();
    return deferred.promise;
  }
  function stepError(n) {
    console.log(n); 
  }

Problem polega na tym, że jeśli nie uda mi się w kroku 1, oba stepError(1)ORAZ stepError(2)są uruchamiane. Jeśli nie return $q.rejectwtedy stepError(2)nie zostanie zwolniony, ale step(2)będzie, co rozumiem. Osiągnąłem wszystko oprócz tego, co próbuję zrobić.

Jak napisać obietnice, aby móc wywołać funkcję w przypadku odrzucenia, bez wywoływania wszystkich funkcji w łańcuchu błędów? Czy jest inny sposób na osiągnięcie tego?

Oto demonstracja na żywo, więc masz coś do zrobienia.

Aktualizacja:

W pewnym sensie to rozwiązałem. Tutaj wyłapuję błąd na końcu łańcucha i przekazuję dane do, reject(data)żeby wiedzieć, jaki problem obsłużyć w funkcji błędu. To właściwie nie spełnia moich wymagań, ponieważ nie chcę polegać na danych. Byłoby kiepskie, ale w moim przypadku czystsze byłoby przekazanie wywołania zwrotnego błędu do funkcji, zamiast polegać na zwracanych danych w celu ustalenia, co należy zrobić.

Demo na żywo tutaj (kliknij).

step(1)
  .then(function() {
    return step(2);
  })
  .then(function() {
    return step(3);
  })
  .then(false, 
    function(x) {
      stepError(x);
    }
  );
  function step(n) {
    console.log('Step '+n);
    var deferred = $q.defer();
    (n === 1) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
  }
  function stepError(n) {
    console.log('Error '+n); 
  }
m59
źródło
1
Istnieje biblioteka async javascript, która może pomóc, jeśli stanie się to bardziej skomplikowane
Lucuma,
Promise.prototype.catch()przykłady na MDN pokazują rozwiązania dla dokładnie tych samych problemów.
toraritte

Odpowiedzi:

199

Powodem, dla którego Twój kod nie działa zgodnie z oczekiwaniami, jest to, że w rzeczywistości robi coś innego niż myślisz, że robi.

Powiedzmy, że masz coś takiego:

stepOne()
.then(stepTwo, handleErrorOne)
.then(stepThree, handleErrorTwo)
.then(null, handleErrorThree);

Aby lepiej zrozumieć, co się dzieje, udajmy, że jest to kod synchroniczny z blokami try/ catch:

try {
    try {
        try {
            var a = stepOne();
        } catch(e1) {
            a = handleErrorOne(e1);
        }
        var b = stepTwo(a);
    } catch(e2) {
        b = handleErrorTwo(e2);
    }
    var c = stepThree(b);
} catch(e3) {
    c = handleErrorThree(e3);
}

Program onRejectedobsługi (drugi argument then) jest zasadniczo mechanizmem korekcji błędów (podobnie jak catchblok). Jeśli zostanie zgłoszony błąd handleErrorOne, zostanie przechwycony przez następny catch block ( catch(e2)) i tak dalej.

To oczywiście nie jest to, co zamierzałeś.

Powiedzmy, że chcemy, aby cały łańcuch rozwiązywania problemów zawiódł bez względu na to, co pójdzie nie tak:

stepOne()
.then(function(a) {
    return stepTwo(a).then(null, handleErrorTwo);
}, handleErrorOne)
.then(function(b) {
    return stepThree(b).then(null, handleErrorThree);
});

Uwaga: możemy zostawić miejsce, w handleErrorOnektórym się znajduje, ponieważ zostanie wywołane tylko wtedy, gdy stepOneodrzuci (jest to pierwsza funkcja w łańcuchu, więc wiemy, że jeśli łańcuch zostanie odrzucony w tym momencie, może to być spowodowane tylko obietnicą tej funkcji) .

Ważną zmianą jest to, że programy obsługi błędów dla innych funkcji nie są częścią głównego łańcucha obietnic. Zamiast tego każdy krok ma swój własny „łańcuch podrzędny”, onRejectedktóry jest wywoływany tylko wtedy, gdy krok został odrzucony (ale nie może być osiągnięty bezpośrednio przez łańcuch główny).

Powodem, dla którego to działa, jest to, że oba onFulfilledi onRejectedsą opcjonalnymi argumentami thenmetody. Jeśli obietnica jest spełniona (tj. Rozwiązana), a następny thenw łańcuchu nie ma onFulfilleduchwytu, łańcuch będzie kontynuowany, dopóki nie będzie takiego z takim handlerem.

Oznacza to, że następujące dwie linie są równoważne:

stepOne().then(stepTwo, handleErrorOne)
stepOne().then(null, handleErrorOne).then(stepTwo)

Ale poniższy wiersz nie jest równoważny z dwoma powyższymi:

stepOne().then(stepTwo).then(null, handleErrorOne)

Biblioteka obietnic $qAngulara jest oparta na Qbibliotece kriskowala (która ma bogatsze API, ale zawiera wszystko, co można znaleźć $q). Dokumentacja Q API na GitHub może okazać się przydatna. Q implementuje specyfikację Promises / A + , która szczegółowo opisuje, w jaki sposób theni jak działa realizacja obietnicy.

EDYTOWAĆ:

Pamiętaj również, że jeśli chcesz wyrwać się z łańcucha w programie obsługi błędów, musi on zwrócić odrzuconą obietnicę lub zgłosić błąd (który zostanie automatycznie przechwycony i zawinięty w odrzuconą obietnicę). Jeśli nie zwrócisz obietnicy, thenzawinie wartość zwrotu w obietnicy rozwiązania dla Ciebie.

Oznacza to, że jeśli nic nie zwrócisz, w rzeczywistości zwracasz rozwiązaną obietnicę wartości undefined.

Alan Plum
źródło
138
Ta część jest złota: if you don't return anything, you are effectively returning a resolved promise for the value undefined.Thanks @pluma
Valerio
7
To jest rzeczywiście.
Edytuję
odrzuca wyjście z bieżącej funkcji? np. postanowienie nie zostanie wywołane, jeśli odrzucenie zostanie nazwane pierwszym `if (złe) {odrzucenie (status); } rozwiązać (wyniki); `
SuperUberDuper
stepOne().then(stepTwo, handleErrorOne) `stepOne (). then (null, handleErrorOne) .then (stepTwo)` Czy są one naprawdę równoważne? Myślę, że w przypadku odrzucenia w stepOnedrugiej linii kodu wykona się, stepTwoale pierwsza tylko wykona handleErrorOnei zatrzyma się. A może coś mi brakuje?
JeFf
5
Naprawdę nie zapewnia jasnego rozwiązania
zadanego
57

Trochę za późno na imprezę, ale to proste rozwiązanie zadziałało:

function chainError(err) {
  return Promise.reject(err)
};

stepOne()
.then(stepTwo, chainError)
.then(stepThreee, chainError);

Pozwala to wyrwać się z łańcucha.

Vinnyq12
źródło
1
Pomogło mi, ale FYI, możesz go zwrócić w tym czasie, aby wyrwać się z połowu, na przykład:.then(user => { if (user) return Promise.reject('The email address already exists.') })
Craig van Tonder
1
@CraigvanTonder, możesz po prostu wrzucić obietnicę i będzie działać tak samo, jak twój kod:.then(user => { if (user) throw 'The email address already exists.' })
Francisco Presencia
1
To jedyna poprawna odpowiedź. W przeciwnym razie krok 3 będzie nadal wykonywany, nawet jeśli krok 1 zawiera błąd.
wdetac
1
Dla wyjaśnienia, jeśli w stepOne () wystąpi błąd, to wywoływane są oba błędy chainError, prawda? Jeśli to pożądane. Mam fragment, który to robi, nie jestem pewien, czy coś źle zrozumiałem - runkit.com/embed/9q2q3rjxdar9
user320550
10

Potrzebujesz powtarzającego się .then()łańcucha ze specjalną skrzynką na początek i specjalną skrzynką na zakończenie.

Umiejętność polega na tym, aby numer kroku przypadku niepowodzenia przeszedł do ostatecznej obsługi błędu.

  • Start: zadzwoń step(1)bezwarunkowo.
  • Powtarzający się wzorzec: połącz a .then()z następującymi wywołaniami zwrotnymi:
    • sukces: wywołanie kroku (n + 1)
    • Failure: wyrzuć wartość, z którą poprzedni deferered został odrzucony lub ponownie zgłoś błąd.
  • Finish: łańcuch a .then()bez procedury obsługi sukcesu i końcowego modułu obsługi błędów.

Możesz napisać całość odręcznie, ale łatwiej jest zademonstrować wzorzec za pomocą nazwanych, uogólnionych funkcji:

function nextStep(n) {
    return step(n + 1);
}

function step(n) {
    console.log('step ' + n);
    var deferred = $q.defer();
    (n === 3) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
}

function stepError(n) {
    throw(n);
}

function finalError(n) {
    console.log('finalError ' + n);
}
step(1)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(null, finalError);});

zobacz demo

Zwróć uwagę, jak w step()przypadku odroczony jest odrzucany lub rozwiązany za pomocą n, dzięki czemu ta wartość jest dostępna dla wywołań zwrotnych w następnym .then()łańcuchu. Po stepErrorwywołaniu błąd jest ponownie zgłaszany, dopóki nie zostanie obsłużony przez finalError.

Burak-Burak
źródło
Pouczająca odpowiedź, więc warto ją zachować, ale to nie jest problem, z którym się zmagam. Wspominam o tym rozwiązaniu w moim poście i nie tego szukam. Zobacz demo u góry mojego postu.
m59
1
m59, to jest odpowiedź na zadane pytanie, "jak mam napisać obietnice, aby móc wywołać funkcję po odrzuceniu, bez wywoływania wszystkich funkcji w łańcuchu błędów?" oraz tytuł pytania „Przerwij łańcuch obietnic i wywołaj funkcję w oparciu o krok w łańcuchu, na którym jest ona zerwana (odrzucona)”
Burak-Burak
Tak, jak powiedziałem, jest to pouczające i nawet zamieściłem to rozwiązanie w moim poście (z mniejszą ilością szczegółów). To podejście ma na celu naprawienie rzeczy, aby łańcuch mógł kontynuować. Chociaż może osiągnąć to, czego szukam, nie jest tak naturalne, jak podejście w zaakceptowanej odpowiedzi. Innymi słowy, jeśli chcesz zrobić to, co wyraża tytuł i zadane pytanie, przyjmij podejście plumy.
m59
7

Podczas odrzucania należy przekazać błąd odrzucenia, a następnie zawinąć procedury obsługi błędów kroku w funkcję, która sprawdza, czy odrzucenie powinno zostać przetworzone, czy „ponownie wrzucone” do końca łańcucha:

// function mocking steps
function step(i) {
    i++;
    console.log('step', i);
    return q.resolve(i);
}

// function mocking a failing step
function failingStep(i) {
    i++;
    console.log('step '+ i + ' (will fail)');
    var e = new Error('Failed on step ' + i);
    e.step = i;
    return q.reject(e);
}

// error handler
function handleError(e){
    if (error.breakChain) {
        // handleError has already been called on this error
        // (see code bellow)
        log('errorHandler: skip handling');
        return q.reject(error);
    }
    // firs time this error is past to the handler
    console.error('errorHandler: caught error ' + error.message);
    // process the error 
    // ...
    //
    error.breakChain = true;
    return q.reject(error);
}

// run the steps, will fail on step 4
// and not run step 5 and 6
// note that handleError of step 5 will be called
// but since we use that error.breakChain boolean
// no processing will happen and the error will
// continue through the rejection path until done(,)

  step(0) // 1
  .catch(handleError)
  .then(step) // 2
  .catch(handleError)
  .then(step) // 3
  .catch(handleError)
  .then(failingStep)  // 4 fail
  .catch(handleError)
  .then(step) // 5
  .catch(handleError)
  .then(step) // 6
  .catch(handleError)
  .done(function(){
      log('success arguments', arguments);
  }, function (error) {
      log('Done, chain broke at step ' + error.step);
  });

Co zobaczysz na konsoli:

step 1
step 2
step 3
step 4 (will fail)
errorHandler: caught error 'Failed on step 4'
errorHandler: skip handling
errorHandler: skip handling
Done, chain broke at step 4

Oto działający kod https://jsfiddle.net/8hzg5s7m/3/

Jeśli masz określoną obsługę każdego kroku, opakowanie może wyglądać następująco:

/*
 * simple wrapper to check if rejection
 * has already been handled
 * @param function real error handler
 */
function createHandler(realHandler) {
    return function(error) {
        if (error.breakChain) {
            return q.reject(error);
        }
        realHandler(error);
        error.breakChain = true;
        return q.reject(error);    
    }
}

to twój łańcuch

step1()
.catch(createHandler(handleError1Fn))
.then(step2)
.catch(createHandler(handleError2Fn))
.then(step3)
.catch(createHandler(handleError3Fn))
.done(function(){
    log('success');
}, function (error) {
    log('Done, chain broke at step ' + error.step);
});
redben
źródło
2

Jeśli dobrze rozumiem, chcesz, aby pojawił się tylko błąd dotyczący kroku, który się nie udał, prawda?

To powinno być tak proste, jak zmiana przypadku niepowodzenia pierwszej obietnicy na to:

step(1).then(function (response) {
    step(2);
}, function (response) {
    stepError(1);
    return response;
}).then( ... )

Wracając $q.reject()w przypadku niepowodzenia pierwszego kroku, odrzucasz tę obietnicę, co powoduje wywołanie errorCallback w drugim then(...).

Zajn
źródło
Co u licha… właśnie to zrobiłem! Zobacz w moim poście, że próbowałem tego, ale łańcuch odskoczył i biec step(2). Teraz po prostu spróbowałem ponownie, to się nie dzieje. Jestem zmieszany.
m59
1
Widziałem, że o tym wspomniałeś. To jednak dziwne. Ta funkcja, która zawiera, return step(2);powinna być wywoływana tylko wtedy, gdy zostanie step(1)pomyślnie rozwiązana.
Zajn
Daj spokój - to zdecydowanie się dzieje. Jak powiedziałem w moim poście, jeśli nie używasz return $q.reject(), łańcuch będzie działał. W tym przypadku wszystko return responseschrzanił. Zobacz: jsbin.com/EpaZIsIp/6/edit
m59
Hmm, dobrze. Wygląda na to, że działa w pliku jsbin, który opublikowałeś, kiedy to zmieniłem, ale musiałem coś przeoczyć.
Zajn
Tak, zdecydowanie widzę, że teraz nie działa. Wróć do deski kreślarskiej dla mnie!
Zajn
2
var s = 1;
start()
.then(function(){
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/20/edit

Lub zautomatyzowane dla dowolnej liczby kroków:

var promise = start();
var s = 1;
var l = 3;
while(l--) {
    promise = promise.then(function() {
        return step(s++);
    });
}
promise.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/21/edit

Esailija
źródło
Ale jeśli zadzwonię deferred.reject(n), otrzymuję ostrzeżenie, że obietnica została odrzucona z obiektem nonError
9me
2

Spróbuj ro, użyj tego jak libs:

https://www.npmjs.com/package/promise-chain-break

    db.getData()
.then(pb((data) => {
    if (!data.someCheck()) {
        tellSomeone();

        // All other '.then' calls will be skiped
        return pb.BREAK;
    }
}))
.then(pb(() => {
}))
.then(pb(() => {
}))
.catch((error) => {
    console.error(error);
});
Leonid
źródło
2

Jeśli chcesz rozwiązać ten problem za pomocą async / await:

(async function(){    
    try {        
        const response1, response2, response3
        response1 = await promise1()

        if(response1){
            response2 = await promise2()
        }
        if(response2){
            response3 = await promise3()
        }
        return [response1, response2, response3]
    } catch (error) {
        return []
    }

})()
luispa
źródło
1

Dołącz procedury obsługi błędów jako oddzielne elementy łańcucha bezpośrednio do wykonywania kroków:

        // Handle errors for step(1)
step(1).then(null, function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).then(null, function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).then(null, function() { stepError(3); return $q.reject(); });
});

lub używając catch():

       // Handle errors for step(1)
step(1).catch(function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).catch(function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).catch(function() { stepError(3); return $q.reject(); });
});

Uwaga: jest to w zasadzie ten sam wzorzec, który sugeruje Pluma w swojej odpowiedzi, ale używając nazewnictwa OP.

Ignitor
źródło
1

Znaleziono Promise.prototype.catch()przykłady na MDN poniżej bardzo pomocne.

(Przyjęta odpowiedź wspomina, then(null, onErrorHandler)która jest w zasadzie taka sama jak catch(onErrorHandler).)

Używanie i łączenie metody catch

var p1 = new Promise(function(resolve, reject) {
  resolve('Success');
});

p1.then(function(value) {
  console.log(value); // "Success!"
  throw 'oh, no!';
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

// The following behaves the same as above
p1.then(function(value) {
  console.log(value); // "Success!"
  return Promise.reject('oh, no!');
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

Problemy przy zgłaszaniu błędów

// Throwing an error will call the catch method most of the time
var p1 = new Promise(function(resolve, reject) {
  throw 'Uh-oh!';
});

p1.catch(function(e) {
  console.log(e); // "Uh-oh!"
});

// Errors thrown inside asynchronous functions will act like uncaught errors
var p2 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    throw 'Uncaught Exception!';
  }, 1000);
});

p2.catch(function(e) {
  console.log(e); // This is never called
});

// Errors thrown after resolve is called will be silenced
var p3 = new Promise(function(resolve, reject) {
  resolve();
  throw 'Silenced Exception!';
});

p3.catch(function(e) {
   console.log(e); // This is never called
});

Jeśli zostanie rozwiązany

//Create a promise which would not call onReject
var p1 = Promise.resolve("calling next");

var p2 = p1.catch(function (reason) {
    //This is never called
    console.log("catch p1!");
    console.log(reason);
});

p2.then(function (value) {
    console.log("next promise's onFulfilled"); /* next promise's onFulfilled */
    console.log(value); /* calling next */
}, function (reason) {
    console.log("next promise's onRejected");
    console.log(reason);
});
toraritte
źródło
1

Najlepszym rozwiązaniem jest refaktoryzacja łańcucha obietnic, tak aby używał ES6 await's. Następnie możesz po prostu wrócić z funkcji, aby pominąć resztę zachowania.

Uderzam głową w ten schemat od ponad roku i używając await's jest niebo.

Pete Alvin
źródło
W przypadku korzystania z czystego programu IE async / await nie jest obsługiwany.
ndee
0

Użyj modułu SequentialPromise

Zamiar

Dostarcz moduł, którego zadaniem jest sekwencyjne wykonywanie żądań, śledząc bieżący indeks każdej operacji w porządku porządkowym. Zdefiniuj operację we wzorcu poleceń, aby zapewnić elastyczność.

Uczestnicy

  • Kontekst : obiekt, którego metoda składowa wykonuje operację.
  • SequentialPromise : definiuje executemetodę łączenia i śledzenia każdej operacji. SequentialPromise zwraca Promise-Chain ze wszystkich wykonanych operacji.
  • Invoker : tworzy instancję SequentialPromise, zapewniając jej kontekst i akcję oraz wywołuje jej executemetodę podczas przekazywania porządkowej listy opcji dla każdej operacji.

Konsekwencje

Użyj SequentialPromise, gdy potrzebne jest porządkowe zachowanie rozwiązania Promise. SequentialPromise będzie śledzić indeks, dla którego odrzucono obietnicę.

Realizacja

clear();

var http = {
    get(url) {
        var delay = Math.floor( Math.random() * 10 ), even = !(delay % 2);
        var xhr = new Promise(exe);

        console.log(`REQUEST`, url, delay);
        xhr.then( (data) => console.log(`SUCCESS: `, data) ).catch( (data) => console.log(`FAILURE: `, data) );

        function exe(resolve, reject) {
            var action = { 'true': reject, 'false': resolve }[ even ];
            setTimeout( () => action({ url, delay }), (1000 * delay) );
        }

        return xhr;
    }
};

var SequentialPromise = new (function SequentialPromise() {
    var PRIVATE = this;

    return class SequentialPromise {

        constructor(context, action) {
            this.index = 0;
            this.requests = [ ];
            this.context = context;
            this.action = action;

            return this;
        }

        log() {}

        execute(url, ...more) {
            var { context, action, requests } = this;
            var chain = context[action](url);

            requests.push(chain);
            chain.then( (data) => this.index += 1 );

            if (more.length) return chain.then( () => this.execute(...more) );
            return chain;
        }

    };
})();

var sequence = new SequentialPromise(http, 'get');
var urls = [
    'url/name/space/0',
    'url/name/space/1',
    'url/name/space/2',
    'url/name/space/3',
    'url/name/space/4',
    'url/name/space/5',
    'url/name/space/6',
    'url/name/space/7',
    'url/name/space/8',
    'url/name/space/9'
];
var chain = sequence.execute(...urls);
var promises = sequence.requests;

chain.catch( () => console.warn(`EXECUTION STOPPED at ${sequence.index} for ${urls[sequence.index]}`) );

// console.log('>', chain, promises);

Sens

SequentialPromise

Cody
źródło
0

Jeśli w którymś momencie wrócisz Promise.reject('something'), zostaniesz wrzucony do bloku do obietnicy.

promiseOne
  .then((result) => {
    if (!result) {
      return Promise.reject('No result');
    }
    return;
  })
  .catch((err) => {
    console.log(err);
  });

Jeśli pierwsza obietnica nie zwróci żadnego wyniku, w konsoli zostanie wyświetlony tylko komunikat „Brak wyniku” .

Dimitar Gospodinov
źródło