8

I want to fulfill a promise with some other promise. The point is that I really want to get access to the (still pending) second promise as soon as the first promise is fulfilled. Unfortunately, I only seem to be able to get the second promise's resolution value once both both promises are fulfilled.

Here's the use case that I have in mind:

var picker = pickFile();

picker.then(  // Wait for the user to pick a file.
    function(downloadProgress) {
        // The user picked a file. The file may not be available just yet (e.g.,
        // if it has to be downloaded over the network) but we can already ask
        // the user some more questions while the file is being obtained in the
        // background.

        ...do some more user interaction...

        return downloadProgress;
    }
).then( // Wait for the download (if any) to complete.
    function(file) {
        // Do something with the file.
    }
)

The function pickFile displays a file picker where the user may pick a file either from their own hard drive or from a URL. It returns a promise picker that is fulfilled as soon as the user has picked a file. At this point, we may still have to download the selected file over the network. Therefore, I cannot fulfill picker with the selected file as resolution value. Instead, picker should be fulfilled with another promise, downloadProgress, which in turn will eventually be fulfilled with the selected file.

For completenes, here's a mock implementation of the pickFile function:

function pickFile() {
    ...display the file picker...

    var resolveP1 = null;

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

    // Mock code to pretend the user picked a file
    window.setTimeout(function() {
        var p2 = Promise.resolve('thefile');
        resolveP1(p2);  // <--- PROBLEM: I actually want to *fulfill* p1 with p2
    }, 3000);

    return p1;
}

The problem in the marked line is that I would like to fulfill the promise p1 with the new promise p2, but I only know how to resolve it. The difference between fulfilling and resolving is that resolving first checks if the supplied value p2 is again a promise. If it is, then fulfillment of p1 will be deferred until p2 is fulfilld, and then p1 will be fulfilled with p2's resolution value instead of p2 itself.

I could work around this issue by building a wrapper around p2, i.e. by replacing the line

        resolveP1(p2);  // <--- PROBLEM: I actually want to *fulfill* p1 with p2

from the second code example by

        resolveP1({promise: p2});

Then, in the first code example, I'd have to replace the line

        return downloadProgress;

by

        return downloadProgress.promise;

But this seems like a bit of a hack when all I really want to do is just fulfill (instead of resolve) a promise.

I'd appreciate any suggestions.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
robamler
  • 179
  • 1
  • 8
  • Promises are unwrapped automatically, the only way to avoid that is to not resolve with another promise, as you mentioned. Is there a downside to resolving with a `result` object initially with the filename or other data initially, then having a `.file` function or property on there that resolves when it has uploaded? – loganfsmyth Aug 23 '15 at 15:58
  • Unfortunately this is not possible with ES6 promises. I fear this will never be fixed, as the workaround is just too simple. – Bergi Aug 23 '15 at 18:58
  • Is your sample a demonstrative scenario or is that your team use case? If that's your user case, your over complicating things. The only async operation here is the download, so have the file promisified, and do the rest in synchronous code. Once all synchronous code is done have a fulfilment function process the file. – Amit Aug 24 '15 at 05:30
  • @Amit: Maybe me calling it a "file picker" was misleading. It's not the modal "open file" dialog associated with an ``. Rather, `pickFile` displays a `
    ` with some input fields to select a file (locally or by URL) and a "continue" button. Thus, making `pickFile` synchronous would freeze the browser and the user wouldn't be able to interact with the form elements. Alternatively, returning a single promise from `pickFile` that is only resolved once the download finishes would mean that the user had to wait for the download even though the file isn't immediately needed.
    – robamler Aug 24 '15 at 18:24
  • @loganfsmyth: I guess, in this specific case, wrapping the returned promise in an object is viable, since I already know that the value I want to pass is a promise. I just thought that I was missing some obvious alternative since I thought fulfilling a promise with a value of _any_ type (be it a promise or not) was a rather generic thing to do and so I didn't expect it to be impossible. (Consider you're writing a library that wants to "return" some user-defined value from an async operation, where the library cannot make any assumptions about the type of the value). Thanks anyway! – robamler Aug 24 '15 at 18:41
  • No that's not what I meant. pickFile is obviously asynchronous, and returns a promise. That's fine. But after that's done, the rest looks like it could be synchronous. If you do that, and only attach the "then" handler once you're done with everything but the download, you'll get the behavior you need – Amit Aug 24 '15 at 18:42
  • Thanks for the quick reply! Unfortunately, I'm still having trouble to understand your solution. Are you suggesting to create the second promise from the main code (i.e. outside the `pickFile` function)? Then, the first promise (`picker`) would have to be resolved with some object that contains all the information needed to initiate the download and the main code would have to know about the implementation of the file picker (e.g., it would have to update a progress bar in the file picker,...). Sure it's possible but it creates a lot of dependencies. Or did I misunderstand your suggestion? – robamler Aug 24 '15 at 18:59
  • In common terminology, "fulfill" means to put a promise in the "success" state (as opposed to "reject"). Given that, you cannot "fulfill" a promise with a promise. You can return a promise from a `.then` handler and that will have the effect of essentially replacing the original promise with the returned promise. By the way, the term "resolve" is usually meant as either of fulfilling or rejecting. –  Aug 29 '15 at 12:19
  • This sort of problem can also happen with anything that has a method called `then()` on it, even if the `then()` method is not the same as a `Promise`'s `then()` and the object is not intended to be a `Promise`. Trying to resolve with it results in the `then()` method being called. – interfect Apr 05 '18 at 05:03

3 Answers3

1

There doesn't seem to be a solution apart from the workaround I already described in the question. For future reference, if you want to fulfill (rather than resolve) a promise p with a value val, where val is another promise, then just calling the promise resolution function for p with argument val won't work as expected. It would cause p to be "locked in" on the state of val, such that p will be fulfilled with val's resolution value once val is fulfilled (see spec).

Instead, wrap val in another object and resolve p with that object:

var resolveP;  // Promise resolution function for p

var p = new Promise(
    function(resolve, reject) {
        resolveP = resolve;
    }
);

function fulfillPwithPromise(val) {  // Fulfills p with a promise val
    resolveP({promise: val});
}

p.then(function(res) {
    // Do something as soon as p is fulfilled...

    return res.promise;
}).then(function(res) {
    // Do something as soon as the second promise is fulfilled...
});

This solution works if you already know that val is a promise. If you cannot make any assumptions about val's type, then you seem to be out of luck. Either you have to always wrap promise resolution values in another object, or you can try to detect whether val has a field then of type "function" and wrap it conditionally.

That said, in some cases the default behavior of promise resolution may actually have the desired effect. So only use the workaround described above if you are sure that you want to fulfill instead of resolve the first promise with the second one.

robamler
  • 179
  • 1
  • 8
  • Probably would be a better idea to wait for more answers before accepting your own. –  Aug 29 '15 at 12:29
  • This Q&A deserves more up-votes. This is a HUGE shortcomming of native `Promise`s. Fortunately, the patch is easy. –  Mar 29 '18 at 08:20
0

Although different people use different terms, in common terminology, "fulfill" means to put a promise in the "success" state (as opposed to "reject")--the state that will trigger then then handlers hanging off it.

In other words, you cannot "fulfill" a promise with a promise. You can fulfill it with a value. (By the way, the term "resolve" is usually meant as either of fulfilling or rejecting.)

What you can do is return a promise from a .then handler and that will have the effect of essentially replacing the original promise with the returned promise.

Here is a simple example of doing that:

asyncTask1 . then(asyncTask2) . then(processData)

where asyncTask1 is a promise, and asyncTask2 is a function which returns a promise. So when asyncTask1 is fulfilled (done successfully), then asyncTask2 runs, and the promise returned by the .then is "taken over" by the promise asyncTask2 returns, so that when it finishes, the data can be processed.

I can do something similar by calling Promise.resolve with a promise as parameter. It's a bit of a misnomer, because I'm not resolving the promise in the technical sense. Instead, the new promise created is "inhabited" by the promise I passed in. It's also useless, because using the result is exactly the same as using the promise I passed in:

Promise.resolve(asyncTask2)

behaves exactly the same as

asyncTask2

(assuming asyncTask2 is already a promise; otherwise Promise.resolve has the effect of creating a promise which is immediately fulfilled with the passed in value.)

Just as you can pass a promise to Promise.resolve, you can pass a promise to the resolve function provided to you as a parameter of the promise constructor callback. If the parameter you pass to resolve is a non-promise, the promise immediately fulfills with that value. However, if the parameter you pass to resolve is another promise, that promise "takes over the body" of the promise you are constructing. To put it another way, the promise you are constructing starts to behave exactly as the the promise passed to resolve.

By "behave exactly" I mean, if the promise you pass in to resolve is already fulfilled, the promise you are constructing is instantly fulfilled with the same value. If the promise you pass in to resolve is already rejected, the promise you are constructing is instantly rejected with the same reason. If the promise you pass in to resolve is not resolved yet, then any then handlers you hang off the promise you are constructing will be invoked if and when the promise you pass to resolve is resolved.

Just as it is confusing that Promise.resolve may result in a promise which is not actually resolved, it is similarly confusing that calling the resolve function handed to you as a parameter to the promise constructor may not actually resolve the promise being constructed if you call it with an unresolved promise. Instead, as I've said a couple of times now, it has the effect of putting the promise being constructed in a state of total congruence with the promise passed to resolve.

Therefore, unless I am missing the point of your question, pickfile could be written as

function pickFile() {
  return new Promise(function(resolve, reject) {
    ...display the file picker...

    // Mock code to pretend the user picked a file
    window.setTimeout(function() {
        resolve('thefile');
    });
}

I didn't really understand your question clearly, so this might not be what you want. Please clarify if you care to.

  • I think the OP does understand the difference between resolving and fulfilling quite well. He does want to fullfill with a promise, as a promise is just a value. – Bergi Aug 29 '15 at 12:49
  • I realize that, colloquially, "fulfilling" and "resolving" a promise are sometimes used as synonyms. I was using the terms in the sense of the [ES6 spec](http://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects), which makes a clear distinction between the two. In this sense, the `resolve` function is named correctly, as the spec states: "A promise is resolved if it is settled or if it has been 'locked in' to match the state of another promise." (where "settled" means "either fulfilled or rejected".) – robamler Aug 30 '15 at 18:44
  • The problem with your implementation is that the file may not be available immediately after the user picked it. If it still has to be downloaded then I want to have the download initiated and monitored by `pickFile`, but I still want to fulfill the promise that is returned by `pickFile` immediately after the user picked a file (the download should continue in the background). – robamler Aug 30 '15 at 18:44
0

Found a similar solution in the process of moving away from Angular's $q to the native Promise feature. Promise.all could be an option (in cases of independent parallel async tasks) by passing around an appropriate object, or something decorated with the state, passing it off to whatever is ready when appropriate. In the Promise.all sample below note how it recovers in one of the promises--took me awhile to realize how to redirect the result of a chain. The result of the all is just the last promise's return. While this doesn't answer the question's title, using return Promise.reject(<an-object-including-a-promise>) (or resolve) gives a series and/or group of async tasks shared access and control along the way. In the case of picking, downloading then working with a file I'd take out the progress-event handling then do: pickFile.then(download,orFailGracefully) with downloadProgress handled within the download onResolve handler (download-progress doesn't appear to be an async task). Below are related experiments in the console.

var q = {
defer: function _defer(){
    var deferred = { };
    deferred.promise = new Promise(function(resolve, reject){
        deferred.resolve = resolve;
        deferred.reject = reject;
    });
    return deferred;
    }
};

var communityThatCares = q.defer();
communityThatCares.promise.then(function(someGood){
    console.log('someGood', someGood);
    return someGood;
}, function(someBad){
    console.warn('someBad', someBad);
    return someBad;
});

(new Promise(function(resolve, reject){ communityThatCares.about = 'communityThatCares'; setTimeout(resolve,1000, communityThatCares); }))
.then(
function(e){
    console.log(3,e); return e.resolve(e);
}, function(e){
    console.warn(3, e); return e.reject(e);
});

var todo = {
    find:'swan'
};

var greaterGood = [(
(new Promise(function(res,rej){ res(todo); })).then(function(e){ e.stuff = 'things'; return e; }),
(new Promise(function(res,reject){ 
    reject(todo);
})).then(function(e){ return e; }
,function(e){
    console.warn(1,e);
    e.recover = 'uh oh';
    return Promise.resolve(e);
}).then(function(e){ console.log(2,e); return e; }),
(new Promise(function(res,rej){ res(todo); })).then(function(e){ console.log(1,e); e.schedule = 'today'; return e; },function(e){ console.warn(1,e); return e; }).then(function(e){ console.log(2,e); return e; }))
];

var nkay = Promise.all( greaterGood )
.then(function(todo){
    console.log('all',todo[0]); return todo;
}, function(todo){
    console.warn('all',todo[0]); return todo;
});
jimmont
  • 2,304
  • 1
  • 27
  • 29
  • May I suggest to [avoid the deferred antipattern](http://stackoverflow.com/q/23803743/1048572), or is this an intentional part of your solution? – Bergi Dec 22 '15 at 12:08
  • Helpful stuff @Bergi. Before an edit: The question is unclear but seems to be "how to resolve (or reject) a promise with another promise or object" which is what my answer tries to address. The sample code doesn't seem to be the best approach for the somewhat vague problem stated, ie mixing progress events with async request/promises doesn't make sense to me. Would it be best remove the answer or just adjust from Promise.all to promise.then.then chain (or something else?)? Thanks! – jimmont Dec 24 '15 at 04:29