Chained promises not passing on rejection

前端 未结 3 1989
挽巷
挽巷 2020-11-22 02:18

I am have a problem understanding why rejections are not passed on through a promise chain and I am hoping someone will be able to help me understand why. To me, attaching f

相关标签:
3条回答
  • 2020-11-22 02:36

    @Jordan firstly as commenters noted, when using deferred lib, your first example definitely produces result you expect:

    promise1 rejected
    promise2 rejected
    promise3 rejected
    

    Secondly, even if it would produce output you suggest, it wouldn't affect execution flow of your second snippet, which is a bit different, more like:

    promise.then(function(first_value) {
        console.log('promise1 resolved');
        var promise = db.put(first_value);
        promise.then(function (second_value) {
             console.log('promise2 resolved');
             var promise = db.put(second_value);
             promise.then(
                 function (wins) { console.log('promise3 resolved'); },
                 function (err) { console.log('promise3 rejected'); return err; });
        }, function (err) { console.log('promise2 rejected'); return err;});
    }, function (err) { console.log('promise1 rejected'); return err});
    

    and that, in case of first promise being rejected will just output:

    promise1 rejected
    

    However (getting to the most interesting part) even though deferred library definitely returns 3 x rejected, most of other promise libraries will return 1 x rejected, 2 x resolved (that leads to assumption you got those results by using some other promise library instead).

    What's additionally confusing, those other libraries are more correct with their behavior. Let me explain.

    In a sync world counterpart of "promise rejection" is throw. So semantically, async deferred.reject(new Error()) in sync equals to throw new Error(). In your example you're not throwing errors in your sync callbacks, you just returning them, therefore you switch to success flow, with an error being a success value. To make sure rejection is passed further, you need to re-throw your errors:

    function (err) { console.log('promise1 rejected'); throw err; });
    

    So now question is, why do deferred library took returned error as rejection?

    Reason for that, is that rejection in deferred works a bit different. In deferred lib the rule is: promise is rejected when it's resolved with an instance of error, so even if you do deferred.resolve(new Error()) it will act as deferred.reject(new Error()), and if you try to do deferred.reject(notAnError) it will throw an exception saying, that promise can be rejected only with instance of error. That makes clear why error returned from then callback rejects the promise.

    There is some valid reasoning behind deferred logic, but still it's not on par with how throw works in JavaScript, and due to that this behavior is scheduled for change with version v0.7 of deferred.

    Short summary:

    To avoid confusion and unexpected results just follow the good practice rules:

    1. Always reject your promises with an error instances (follow rules of sync world, where throwing value that's not an error is considered a bad practice).
    2. Reject from sync callbacks by throwing errors (returning them doesn't guarantee rejection).

    Obeying to above, you'll get both consistent and expected results in both deferred and other popular promise libraries.

    0 讨论(0)
  • 2020-11-22 02:37

    To me, this result doesn't make sense. By attaching to this promise chain, each then is implying the intent that it will be dependant upon the successful resolution of d1 and a result being passed down the chain

    No. What you are describing is not a chain, but just attaching all the callbacks to d1. Yet, if you want to chain something with then, the result for promise2 is dependent on the resolution of promise1 and how the then callbacks handled it.

    The docs state:

    Returns a new promise for the result of the callback(s).

    The .then method is usually looked upon in terms of the Promises/A specification (or the even stricter Promsises/A+ one). That means the callbacks shell return promises which will be assimilated to become the resolution of promise2, and if there is no success/error handler the respective result will in case be passed directly to promise2 - so you can simply omit the handler to propagate the error.

    Yet, if the error is handled, the resulting promise2 is seen as fixed and will be fulfilled with that value. If you don't want that, you would have to re-throw the error, just like in a try-catch clause. Alternatively you can return a (to-be-)rejected promise from the handler. Not sure what Dojo way to reject is, but:

    var d1 = d();
    
    var promise1 = d1.promise.then(
        function(wins) { console.log('promise1 resolved'); return wins;},
        function(err) { console.log('promise1 rejected'); throw err;});
    var promise2 = promise1.then(
        function(wins) { console.log('promise2 resolved'); return wins;},
        function(err) { console.log('promise2 rejected'); throw err;});
    var promise3 = promise2.then(
        function(wins) { console.log('promise3 resolved'); return wins;},
        function(err) { console.log('promise3 rejected'); throw err;});
    d1.reject(new Error());
    

    How is Bob able to get a blue widget from Ginger when didn't get any herself?

    He should not be able. If there are no error handlers, he will just perceive the message (((from the distributor) from John) from Ginger) that there are no widgets left. Yet, if Ginger sets up an error handler for that case, she still might fulfill her promise to give Bob a widget by giving him a green one from her own shack if there are no blue ones left at John or his distributor.

    To translate your error callbacks into the metapher, return err from the handler would just be like saying "if there are no widgets left, just give him the note that there are no ones left - it's as good as the desired widget".

    In the database situation, if the db.query failed, it would call the err function of the first then

    …which would mean that the error is handled there. If you don't do that, just omit the error callback. Btw, your success callbacks don't return the promises they are creating, so they seem to be quite useless. Correct would be:

    var promise = db.query({parent_id: value});
    promise.then(function(query_result) {
        var first_value = {
            parent_id: query_result[0].parent_id
        }
        var promise = db.put(first_value);
        return promise.then(function(first_value_result) {
            var second_value = {
                reference_to_first_value_id: first_value_result.id
            }
            var promise = db.put(second_value);
            return promise.then(function(second_value_result) {
                return values_successfully_entered();
            });
        });
    });
    

    or, since you don't need the closures to access result values from previous callbacks, even:

    db.query({parent_id: value}).then(function(query_result) {
        return db.put({
            parent_id: query_result[0].parent_id
        });
    }).then(function(first_value_result) {
        return db.put({
            reference_to_first_value_id: first_value_result.id
        });
    }.then(values_successfully_entered);
    
    0 讨论(0)
  • 2020-11-22 02:47

    Use can wrap the errors at each level of the Promise. I chained the errors in TraceError:

    class TraceError extends Error {
      constructor(message, ...causes) {
        super(message);
    
        const stack = Object.getOwnPropertyDescriptor(this, 'stack');
    
        Object.defineProperty(this, 'stack', {
          get: () => {
            const stacktrace = stack.get.call(this);
            let causeStacktrace = '';
    
            for (const cause of causes) {
              if (cause.sourceStack) { // trigger lookup
                causeStacktrace += `\n${cause.sourceStack}`;
              } else if (cause instanceof Error) {
                causeStacktrace += `\n${cause.stack}`;
              } else {
                try {
                  const json = JSON.stringify(cause, null, 2);
                  causeStacktrace += `\n${json.split('\n').join('\n    ')}`;
                } catch (e) {
                  causeStacktrace += `\n${cause}`;
                  // ignore
                }
              }
            }
    
            causeStacktrace = causeStacktrace.split('\n').join('\n    ');
    
            return stacktrace + causeStacktrace;
          }
        });
    
        // access first error
        Object.defineProperty(this, 'cause', {value: () => causes[0], enumerable: false, writable: false});
    
        // untested; access cause stack with error.causes()
        Object.defineProperty(this, 'causes', {value: () => causes, enumerable: false, writable: false});
      }
    }
    

    Usage

    throw new TraceError('Could not set status', srcError, ...otherErrors);
    

    Output

    Functions

    TraceError#cause - first error
    TraceError#causes - list of chained errors
    
    0 讨论(0)
提交回复
热议问题