Break javascript promise chain in a clean way

2019-04-21 07:38发布

问题:

I am trying to chain promises so that the chain will break if one promise is rejected. I followed the leads of a previous SO question and tried to apply it to native promises, but I think I am misunderstanding the way things work.

Here is how I have rewritten the code:

Promise.resolve()
    .then(function() {
        return step(1)
            .then(null, function() {
                stepError(1);
            });
    })
    .then(function() {
        return step(2)
            .then(null, function() {
                stepError(2);
            });
    })
    .then(function() {
        return step(3)
            .then(null, function() {
                stepError(3);
            });
    });

function step(n) {
    console.log('Step '+n);
    return (n === 2) ? Promise.reject(n) : Promise.resolve(n);
}

function stepError(n) {
    console.log('Error '+n);
    return Promise.reject(n);
}

The output of the above code is:

Step 1
Step 2
Error 2
Step 3
[UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): 2]

In my understanding, step 2 should break the chain and step 3 should not be executed. When step(2) returns a rejected promise, stepError(2) is executed as expected. But since it returns Promise.reject(2), the function in the next then should not be executed, and since there is not catch in the end, the rejected promise of step 2 seems - as expected - to be forwarded until it exits the chain because it didn't find any handler.

What am I missing here ?

Here is a JSFiddle to play with: https://jsfiddle.net/6p4t9xyk/

回答1:

In my understanding, step 2 should break the chain...

It would, but you've accidentally converted that rejection into a resolution.

The key thing about promises is that every call to then creates a new promise which is resolved/rejected based on what the then callback(s) do, and the callback processing a rejection converts that rejection into a resolution unless it intentionally does otherwise.

So here:

return step(2)
    .then(null, function() {  // This handler converts the
        stepError(2);         // rejection into a resolution
    });                       // with the value `undefined`

That's so that you can have error handlers that compensate for the error.

Since stepError returns a rejection, you could continue the rejection by just adding a return:

return step(2)
    .then(null, function() {
        return stepError(2);  // Added `return`
    });

...or alternately, remove that handler entirely:

return step(2);

...or you could throw in the callback, which is automatically turned into a rejection.

The unhandled rejection warning is caused by the fact nothing consumes the rejection from stepError.


Here's an example returning the result of stepError:

Promise.resolve()
    .then(function() {
        return step(1)
            .then(null, function() {
                return stepError(1); // Added `return`
            });
    })
    .then(function() {
        return step(2)
            .then(null, function() {
                return stepError(2); // Added `return`
            });
    })
    .then(function() {
        return step(3)
            .then(null, function() {
                return stepError(3); // Added `return`
            });
    });

function step(n) {
    console.log('Step '+n);
    return (n === 2) ? Promise.reject(n) : Promise.resolve(n);
}

function stepError(n) {
    console.log('Error '+n);
    return Promise.reject(n);
}



回答2:

Looks like you overcomplicated it quite a bit. How about:

function step(n) {
    console.log('Step '+n);
    return (n === 2) ? Promise.reject(n) : Promise.resolve(n);
}

function stepError(n) {
    console.log('Error '+n);
}

Promise.resolve()
    .then(() => step(1))
    .then(() => step(2))
    .then(() => step(3)) // never here
    .catch(stepError);

In general, "indented then" is an antipattern. All "thens" should be on the same level.



回答3:

As @T.J.Crowder said, you forgot to return the result of the error handler (or to throw from it). To fix it, I'd recommend doing either

function withStepError(n, promise) {
    return promise.catch(function(err) {
        console.log('Error '+err+' from '+n);
        throw new Error("failed at "+n);
    });
}
Promise.resolve()
.then(function() {
    return withStepError(1, step(1));
})
.then(function() {
    return withStepError(2, step(2));
})
.then(function() {
    return withStepError(3, step(3));
});

or

function getStepError(n) {
    return function(err) {
        console.log('Error '+err+' from '+n);
        throw new Error("failed at "+n);
    };
}

Promise.resolve()
.then(function() {
    return step(1).catch(getStepError(1));
})
.then(function() {
    return step(2).catch(getStepError(2));
})
.then(function() {
    return step(3).catch(getStepError(3));
});