6

I want to use the when.map function to process some data. After the data is processed I need to do some cleanup (e.g. release the currently used database connection back to the connection pool).

The problem with my approach using catch and finally is that finally is called when the first reject occurs, and while other mappings are still in progress.

So how can I wait until all of the mapping promises are finished, so that it is possible to do a save cleanup.

  require('when/monitor/console');
  var when = require('when');

  function testMapper(value) {
    console.log('testMapper called with: '+value);
    return when.promise(function(resolve, reject) {
      setTimeout(function() {
        console.log('reject: '+value);
        reject(new Error('error: '+value));
      },100*value);
    });
  }

  when.map([1,2,3,4],testMapper)
  .then(function() {
    console.log('finished')
  })
  .catch(function(e) {
    console.log(e);
  })
  .finally(function() {
    console.log('finally')
  });

Output

 testMapper called with: 1
 testMapper called with: 2
 testMapper called with: 3
 testMapper called with: 4
 reject: 1
 [Error: error: 1]
 finally
 reject: 2
 [promises] Unhandled rejections: 1
 Error: error: 2
     at null._onTimeout (index.js:9:14)

 reject: 3
 [promises] Unhandled rejections: 2
 Error: error: 2
     at null._onTimeout (index.js:9:14)

 Error: error: 3
     at null._onTimeout (index.js:9:14)

 reject: 4
 [promises] Unhandled rejections: 3
 Error: error: 2
     at null._onTimeout (index.js:9:14)

 Error: error: 3
     at null._onTimeout (index.js:9:14)

 Error: error: 4
     at null._onTimeout (index.js:9:14)  

Environmentinformation:

  • whenjs: v3.1.0
  • node: v0.10.26
t.niese
  • 39,256
  • 9
  • 74
  • 101

2 Answers2

2

Your best bet is to go with when.settle, settle returns all promises when they resolve, not when they fulfill so you can manually check which one did well and which one did not.

var arrayOfPromises = array.map(testMapper);
when.settle(arrayOfPromises).then(function(descriptors){
     descriptors.forEach(function(d){
         if(d.state === "rejected"){
             // do cleanup for that promise, you can access its rejection reason here
             // and do any cleanup you want
         } else{
            // successful results accessed here
            console.log("Successful!", d.value);
         }
     })
});

Note:

This is actually not a small problem. When I say not a small problem, what I mean is that it's a huge problem that's really hard to solve correctly. There are multiple implied behaviors here and edge cases.

Consider reading this somewhat lengthy discussion. If you're willing to consider - Bluebird has an experimental promise-using branch that allows specifying disposers, which would let you do this rather easily.

You would be able to do:

using(pool.getConnectionAsync().disposer("close"), function(connection) {
   return connection.queryAsync("SELECT * FROM TABLE");
}).then(function(rows) {
    console.log(rows);
});

Or in the multiple resource case:

var a = Promise.cast(externalPromiseApi.getResource1()).disposer("close");
var b = Promise.cast(externalPromiseApi.getResource2()).disposer("close");
using(a, b, function(resource1, resource2) {
    // once the promise returned here is resolved, we have a deterministic guarantee that 
    // all the resources used here have been closed.
})
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • I'll take a closer look at `settle`. But it is definitely not a behavior that I would have expected, especially as it is compared with the classical `try - catch - finally` block in the docs. Every time I think that I understood everything about _promise_ then have to find out that there are even more stumbling blocks :D. – t.niese Apr 15 '14 at 16:26
  • @t.niese the problem isn't similar to try/catch/finally in general. The problem is resource acquisition in general, resource acquisition is a huge issue in programming. For example, see copying a file in Java with try/catch/finally http://stackoverflow.com/a/909376/1348195 - with `try-resource` in Java, you could do `try(resource1,resource2){ /* resources get disposed if fail here */}`. The goal of `Promise.using` is to give you the same level of ease of use only with asynchronous resource acquisitions and operations. Not an easy task. – Benjamin Gruenbaum Apr 15 '14 at 18:19
  • For an example of a problem - here is Eric Lippert's answer to a conceptual issue we've faced when discussing `Promise.using` - http://stackoverflow.com/a/21123756/1348195 . We solved this particular problem by not allowing disposers to throw. – Benjamin Gruenbaum Apr 15 '14 at 18:19
  • I understand the problem. I think I had the wrong expectations while reading the docs and because of that I have seen the operations like `when.map`, `when.join`, `when.all`, ... as somehow _atomic_, that the only will `reject` if none of the _promieses_ passed to them are in a `pending` state anymore, like it is for the `resolve`. While I could understand that the `catch` may be called immediately, I still would expect that `finally` should wait until all of the promises are not in `pending` state anymore. Anyway thx for the time you spend answering the question. – t.niese Apr 15 '14 at 19:10
  • @t.niese For what it's worth I feel your pain. I think the using construct is definitely the way to go. Glad I could help you with the issue. – Benjamin Gruenbaum Apr 15 '14 at 19:20
  • I solve my problem for now, by writing a _settle.map_ out of your answer. And now have time to look into `bluebird`. – t.niese Apr 15 '14 at 21:32
0

Based on the answer of Benjamin Gruenbaum I create a replacement of when.map that uses settle internally, and will trigger the cache and finally when all promises of the map have been handled.

  var settle = {};
  var arrayMap = Array.prototype.map;

  settle.map = function(array, f) {

    var arrayOfPromises = arrayMap.call(array,function(x) {
      return when.resolve(x).then(f);
    });

    return when.settle(arrayOfPromises)
    .then(function(descriptors) {
      var result = [];

      descriptors.forEach(function(descriptor) {
        if( descriptor.state === 'rejected') {
          throw descriptor.reason;
        }

        result.push(descriptor.value);
      });

      return result;
    });
  };

When I now replace when.map with settle.map in my original code, then the output/execution order is as I require it:

 testMapper called with: 1
 testMapper called with: 2
 testMapper called with: 3
 testMapper called with: 4
 reject: 1
 reject: 2
 reject: 3
 reject: 4
 [Error: error: 1]
 finally
t.niese
  • 39,256
  • 9
  • 74
  • 101