This post will teach you how to write promise-based mongoose code, using Kris Kowal’s Q library.
You can also find the code below in the `src/mongoose-and-q` dir of my blog code repository.
We have a mongo database with 3 collections: users, bicycles and cars. Bicycles and cars are owned by users. We will write a script that shows the bicycles and cars owned by rupert@example.com .
We’ll look at multiple versions of this script. This article focuses on callbacks and promises, so we’ll be using a hardcoded email address and we’ll not be handling errors very well. Those aspects of the code are for a different article.
The first version uses callbacks for the database calls.
'use strict'; var mongoose = require('mongoose'); var Schema = mongoose.Schema; var UserSchema = new Schema({ email : { type: String }, firstName: { type: String }, }); var User = mongoose.model('User', UserSchema); var CarSchema = new Schema({ ownerId : { type: Schema.Types.ObjectId, ref: 'User' }, colour : { type: String }, numberOfWheels: { type: Number }, }); var Car = mongoose.model('Car', CarSchema); var BicycleSchema = new Schema({ ownerId : { type: Schema.Types.ObjectId, ref: 'User' }, colour : { type: String }, numberOfWheels: { type: Number }, }); var Bicycle = mongoose.model('Bicycle', BicycleSchema); mongoose.connect('mongodb://localhost/blogtest'); var email = 'rupert@example.com'; function getVehicles(email, cb) { User.findOne({ email: email}, function(err, user) { if (err) { return cb(err); } Car.find({ ownerId: user._id }, function(err, cars) { if (err) { return cb(err); } Bicycle.find({ ownerId: user._id }, function(err, bicycles) { if (err) { return cb(err); } cb(null, { cars: cars, bicycles: bicycles }); }); }); }); } getVehicles(email, function(err, vehicles) { if (err) { console.error('Something went wrong: ' + err); } else { console.info(vehicles); } mongoose.disconnect(); });
Two things stand out in this code. First, the indenting in getVehicles . It looks bad. It is bad. And if we want to add more callbacks, it gets worse. Second, we are using error handling code in each individual callback.
We can rewrite this code to use Q .
'use strict'; var Q = require('q'); var mongoose = require('mongoose'); var Schema = mongoose.Schema; var UserSchema = new Schema({ email : { type: String }, firstName: { type: String }, }); var User = mongoose.model('User', UserSchema); var CarSchema = new Schema({ ownerId : { type: Schema.Types.ObjectId, ref: 'User' }, colour : { type: String }, numberOfWheels: { type: Number }, }); var Car = mongoose.model('Car', CarSchema); var BicycleSchema = new Schema({ ownerId : { type: Schema.Types.ObjectId, ref: 'User' }, colour : { type: String }, numberOfWheels: { type: Number }, }); var Bicycle = mongoose.model('Bicycle', BicycleSchema); mongoose.connect('mongodb://localhost/blogtest'); var email = 'rupert@example.com'; function getVehicles(email) { var foundCars, foundUser; return Q(User.findOne({ email: email }).exec()) .then(function(user) { foundUser = user; return Q(Car.find({ ownerId: user._id }).exec()) }) .then(function(cars) { foundCars = cars; return Q(Bicycle.find({ ownerId: foundUser._id }).exec()) }) .then(function(bicycles) { return { bicycles: bicycles, cars: foundCars }; }); } getVehicles(email) .then(function(vehicles) { console.log(vehicles); }) .catch(function(err) { console.error('Something went wrong: ' + err); }) .done(function() { mongoose.disconnect(); });
The getVehicles function now returns the promise created on line 33. The user is found, then the cars, then the bicycles, and then the promise resolves to the same object as in the first listing.
This version of getVehicles looks better regarding both indenting and error handling. The indenting stays on the same level, and the error handling – no matter where the error happens – is done by the catch block after calling getVehicles .
However, there is also a disadvantage to this code. The vars from a then block can’t be used in the next then block, because each consecutive then block has its own function scope. That’s why we have to assign user and cars to helper vars outside their scope.
We can do better. Let’s look at our improved version of getVehicles .
function getVehicles(email) { return Q(User.findOne({ email: email }).exec()) .then(function(user) { return Q.all([ Q(Bicycle.find({ ownerId: user._id }).exec()), Q(Car.find({ ownerId: user._id }).exec()) ]); }) .then(function(results) { return { bicycles: results[0], cars: results[1] }; }); }
We use Q.all(arrayOfPromises) to retrieve both the cars and the bicycles in one go. Q.all resolves to a list of fulfilment values, which end up in an array in the then block.
This both makes the code shorter and gets rid of the helper variables. There is one minor improvement that we can still make.
function getVehicles(email) { return Q(User.findOne({ email: email }).exec()) .then(function(user) { return [ Q(Bicycle.find({ ownerId: user._id }).exec()), Q(Car.find({ ownerId: user._id }).exec()) ]; }) .spread(function(bicycles, cars) { return { bicycles: bicycles, cars: cars }; }); }
We replace .then by .spread , so the bicycles and cars end up in one argument each, instead of an array of results. Additionally, because .spread calls .all initially, we can now remove Q.all and return just the array of promises. This code is more readable and maintainable than the previous version.
4 Responses to Combining mongoose and Q in node.js