112

I want to drop some mongodb collections, but that's an asynchronous task. The code will be:

var mongoose = require('mongoose');

mongoose.connect('mongo://localhost/xxx');

var conn = mongoose.connection;

['aaa','bbb','ccc'].forEach(function(name){
    conn.collection(name).drop(function(err) {
        console.log('dropped');
    });
});
console.log('all dropped');

The console displays:

all dropped
dropped
dropped
dropped

What is the simplest way to make sure all dropped will be printed after all collections has been dropped? Any 3rd-party can be used to simplify the code.

apsillers
  • 112,806
  • 17
  • 235
  • 239
Freewind
  • 193,756
  • 157
  • 432
  • 708

8 Answers8

129

Use Promises.

var mongoose = require('mongoose');

mongoose.connect('your MongoDB connection string');
var conn = mongoose.connection;

var promises = ['aaa', 'bbb', 'ccc'].map(function(name) {
  return new Promise(function(resolve, reject) {
    var collection = conn.collection(name);
    collection.drop(function(err) {
      if (err) { return reject(err); }
      console.log('dropped ' + name);
      resolve();
    });
  });
});

Promise.all(promises)
.then(function() { console.log('all dropped)'); })
.catch(console.error);

This drops each collection, printing “dropped” after each one, and then prints “all dropped” when complete. If an error occurs, it is displayed to stderr.


Previous answer (this pre-dates Node’s native support for Promises):

Use Q promises or Bluebird promises.

With Q:

var Q = require('q');
var mongoose = require('mongoose');

mongoose.connect('your MongoDB connection string');
var conn = mongoose.connection;

var promises = ['aaa','bbb','ccc'].map(function(name){
    var collection = conn.collection(name);
    return Q.ninvoke(collection, 'drop')
      .then(function() { console.log('dropped ' + name); });
});

Q.all(promises)
.then(function() { console.log('all dropped'); })
.fail(console.error);

With Bluebird:

var Promise = require('bluebird');
var mongoose = Promise.promisifyAll(require('mongoose'));

mongoose.connect('your MongoDB connection string');
var conn = mongoose.connection;

var promises = ['aaa', 'bbb', 'ccc'].map(function(name) {
  return conn.collection(name).dropAsync().then(function() {
    console.log('dropped ' + name);
  });
});

Promise.all(promises)
.then(function() { console.log('all dropped'); })
.error(console.error);
Nate
  • 18,752
  • 8
  • 48
  • 54
  • 1
    Promises are the way to go. [Bluebird](https://github.com/petkaantonov/bluebird) is another promise library that would work well if this is in performance-critical code. It should be a drop-in replacement. Just use `require('bluebird')`. – weiyin Aug 01 '14 at 20:08
  • I’ve added a Bluebird example. It’s a little different since the best way to use Bluebird is to use the `promisifyAll` feature. – Nate Sep 09 '14 at 22:01
  • Any idea how promisifyAll works..I've read docs but i dont get it is that how it handles functions that don't parameters like `function abc(data){`, because it's not like `function abc(err, callback){...` Basically i dont think all functions take error as first param and callback as 2nd param – Muhammad Umer Apr 12 '15 at 21:28
  • @MuhammadUmer Lots of detail at http://bluebirdjs.com/docs/api/promise.promisifyall.html – Nate Mar 21 '16 at 19:17
  • It's been a while since the MongoDB driver supports promises as well. Can you update your example to take advantage of this? `.map(function(name) { return conn.collection(name).drop() })` – djanowski Sep 20 '16 at 18:42
91

I see you are using mongoose so you are talking about server-side JavaScript. In that case I advice looking at async module and use async.parallel(...). You will find this module really helpful - it was developed to solve the problem you are struggling with. Your code may look like this

var async = require('async');

var calls = [];

['aaa','bbb','ccc'].forEach(function(name){
    calls.push(function(callback) {
        conn.collection(name).drop(function(err) {
            if (err)
                return callback(err);
            console.log('dropped');
            callback(null, name);
        });
    }
)});

async.parallel(calls, function(err, result) {
    /* this code will run after all calls finished the job or
       when any of the calls passes an error */
    if (err)
        return console.log(err);
    console.log(result);
});
GabLeRoux
  • 16,715
  • 16
  • 63
  • 81
freakish
  • 54,167
  • 9
  • 132
  • 169
  • With this... the forEach method happens async. So if the object list was longer than the 3 detailed here, could it not be the case that when async.parallel(calls, function(err, result) is evaluated calls doesn't yet contain all of the functions in the original list? – Martin Beeby Jun 11 '13 at 20:59
  • 5
    @MartinBeeby `forEach` is synchronous. Have a look here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach There's implementation of `forEach` at the bottom. Not everything with callback is asynchronous. – freakish Jun 11 '13 at 23:37
  • 2
    For the record, async can also be used in a browser. – Erwin Wessels Mar 27 '14 at 06:43
  • @MartinBeeby Everything with a callback IS asynchronous, the problem is that forEach is not being passed a "callback", but just a regular function (which is incorrect use of terminology by Mozilla). In a functional programming language, you would never call a passed function a "callback" –  Jun 06 '17 at 16:48
  • 3
    @ghert85 No, there's nothing wrong with the terminology. Callback is simply any executable code that is passed as an argument to other code and is expected to be executed at some point. That's the standard definition. And it can be called synchronously or asynchronously. See this: https://en.wikipedia.org/wiki/Callback_(computer_programming) – freakish Jun 06 '17 at 20:17
21

The way to do it is to pass the tasks a callback that updates a shared counter. When the shared counter reaches zero you know that all tasks have finished so you can continue with your normal flow.

var ntasks_left_to_go = 4;

var callback = function(){
    ntasks_left_to_go -= 1;
    if(ntasks_left_to_go <= 0){
         console.log('All tasks have completed. Do your stuff');
    }
}

task1(callback);
task2(callback);
task3(callback);
task4(callback);

Of course, there are many ways to make this kind of code more generic or reusable and any of the many async programing libraries out there should have at least one function to do this kind of thing.

Community
  • 1
  • 1
hugomg
  • 68,213
  • 24
  • 160
  • 246
  • This might not be the easiest to implement, but I really like seeing an answer that doesn't require external modules. Thank you! – counterbeing Dec 09 '13 at 13:31
8

Expanding upon @freakish answer, async also offers a each method, which seems especially suited for your case:

var async = require('async');

async.each(['aaa','bbb','ccc'], function(name, callback) {
    conn.collection(name).drop( callback );
}, function(err) {
    if( err ) { return console.log(err); }
    console.log('all dropped');
});

IMHO, this makes the code both more efficient and more legible. I've taken the liberty of removing the console.log('dropped') - if you want it, use this instead:

var async = require('async');

async.each(['aaa','bbb','ccc'], function(name, callback) {
    // if you really want the console.log( 'dropped' ),
    // replace the 'callback' here with an anonymous function
    conn.collection(name).drop( function(err) {
        if( err ) { return callback(err); }
        console.log('dropped');
        callback()
    });
}, function(err) {
    if( err ) { return console.log(err); }
    console.log('all dropped');
});
Erwin Wessels
  • 2,972
  • 1
  • 24
  • 17
5

I do this without external libaries:

var yourArray = ['aaa','bbb','ccc'];
var counter = [];

yourArray.forEach(function(name){
    conn.collection(name).drop(function(err) {
        counter.push(true);
        console.log('dropped');
        if(counter.length === yourArray.length){
            console.log('all dropped');
        }
    });                
});
user435943
  • 974
  • 10
  • 14
4

All answers are quite old. Since the beginning of 2013 Mongoose started to support promises gradually for all queries, so that would be the recommended way of structuring several async calls in the required order going forward I guess.

Capaj
  • 4,024
  • 2
  • 43
  • 56
0

With deferred (another promise/deferred implementation) you can do:

// Setup 'pdrop', promise version of 'drop' method
var deferred = require('deferred');
mongoose.Collection.prototype.pdrop =
    deferred.promisify(mongoose.Collection.prototype.drop);

// Drop collections:
deferred.map(['aaa','bbb','ccc'], function(name){
    return conn.collection(name).pdrop()(function () {
      console.log("dropped");
    });
}).end(function () {
    console.log("all dropped");
}, null);
Mariusz Nowak
  • 32,050
  • 5
  • 35
  • 37
0

If you are using Babel or such transpilers and using async/await you could do :

function onDrop() {
   console.log("dropped");
}

async function dropAll( collections ) {
   const drops = collections.map(col => conn.collection(col).drop(onDrop) );
   await drops;
   console.log("all dropped");
}
ganaraj
  • 26,841
  • 6
  • 63
  • 59
  • You can't pass a callback to `drop()` and expect to return a Promise. Can you please fix this example and remove `onDrop`? – djanowski Sep 20 '16 at 18:39