Mongoose 5.8.0
was released on December 9, 2019. 5.8.0 is a semver minor version,
which means it introduces several new features. There are 15 new features in Mongoose 5.8.0,
ranging from a pre-compiled browser bundle to several index management improvements. In
this article, I'll cover two of my favorite new features: the Schema#pick()
function
and improved stack traces for server selection timeout errors.
The Schema#pick()
Function
The Schema#pick()
function is analagous to Lodash's pick()
function for Mongoose schemas. Given a
schema, pick()
creates a new schema with a subset of the original schema's
paths.
For example, given a schema with 3 paths, name
, email
, and age
,
the pick()
function lets you create a new schema with just name
and age
.
const schema = new Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
age: {
type: Number,
required: true
}
});
const newSchema = schema.pick(['name', 'age']);
newSchema.path('name'); // SchemaString { ... }
newSchema.path('age'); // SchemaNumber { ... }
newSchema.path('email'); // undefined
The newly created schema's paths have the same options for the picked paths.
In newSchema
above, name
and age
are both required, just as
they are in schema
.
The Schema#pick()
function also works on nested paths. For example, suppose you store name
as an object,
with first
and last
as nested properties:
const schema = new Schema({
name: {
first: {
type: String,
required: true
},
last: {
type: String,
required: true
}
},
email: {
type: String,
required: true
},
age: {
type: Number,
required: true
}
});
Given this schema
, you can create a schema that just contains all the
sub-properties of name
:
const newSchema = schema.pick(['name']);
newSchema.path('name.first'); // SchemaString { ... }
newSchema.path('name.last'); // SchemaString { ... }
newSchema.path('age'); // undefined
Or, you can create a schema that just contains age
and name.first
:
const newSchema = schema.pick(['name.first', 'age']);
newSchema.path('name.first'); // SchemaString { ... }
newSchema.path('age'); // SchemaNumber { ... }
newSchema.path('name.last'); // undefined
Better Stack Traces for "Server Selection Timed Out After..." Errors
The useUnifiedTopology
option
for Mongoose connections significantly changes how the underlying MongoDB driver
handles connections. Here's a brief summary:
- Before
useUnifiedTopology
, the MongoDB driver handled connectivity loss differently depending on whether you were connected to a single server, replica set, or sharded cluster. Furthermore, the behavior was slightly different depending on whether you hadn't connected yet, the connection was interrupted, or you had explicitly disconnected. - With
useUnifiedTopology
, the MongoDB driver has a single algorithm for trying to send an operation to a server, regardless of topology and regardless of whether that operation is an initial connection, a query, or an index build. That process is called server selection, and if server selection times out, you get aMongoTimeoutError
.
The major downside of a unified algorithm with only one type of error is how hard it is to figure out what caused the error. With JavaScript, the problem is even worse unless you have async stack traces. For example, by default this is the error message and stack trace you get when server selection times out:
MongoTimeoutError: Server selection timed out after 250 ms
at Timeout.setTimeout [as _onTimeout] (/mongoose/node_modules/mongodb/lib/core/sdam/server_selection.js:309:9)
at ontimeout (timers.js:436:11)
at tryOnTimeout (timers.js:300:5)
at listOnTimeout (timers.js:263:5)
at Timer.processTimers (timers.js:223:10)
You get the same message and stack regardless of whether this error occurred due
to mongoose.connect()
or Model.find()
or Model.syncIndexes()
. For example,
below is one script that prints the above error message:
const mongoose = require('mongoose');
// Fails because `baddomain` isn't a valid hostname.
mongoose.connect('mongodb://baddomain:27017/test', {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 250,
bufferCommands: false
}).catch(err => console.log(err));
Below is another, different script that prints the same error message and stack trace:
const Server = require('mongodb-topology-manager').Server;
const mongoose = require('mongoose');
run().catch(err => console.log(err));
async function run() {
// Start a MongoDB server on port 27000 and connect Mongoose to it
let server = new Server('mongod', {
port: 27000,
dbpath: './27000'
});
await server.start();
await mongoose.connect('mongodb://localhost:27000/test', {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 250
});
const Model = mongoose.model('Test', mongoose.Schema({ name: String }));
// Try to execute a query when the MongoDB server is down
await server.stop();
await Model.findOne().exec();
}
As a side note, MongoTimeoutError
instances have a reason
property that tells you why server selection timed out.
In Mongoose 5.8, if you execute a query using .exec()
, you get a more useful
stack trace when a MongoTimeoutError
occurs. Assuming the above script
is contained in a file script.js
, below is the stack trace you'll see.
The first entry in the stack trace is the line in script.js
that calls
exec()
.
MongoTimeoutError: Server selection timed out after 250 ms
at new MongooseTimeoutError (node_modules/mongoose/lib/error/timeout.js:22:11)
at Function.Model.$wrapCallback (/node_modules/mongoose/lib/model.js:4778:19)
at utils.promiseOrCallback (/node_modules/mongoose/lib/query.js:4358:21)
at Promise (/node_modules/mongoose/lib/utils.js:283:5)
at new Promise (<anonymous>)
at Object.promiseOrCallback (/node_modules/mongoose/lib/utils.js:282:10)
at model.Query.exec (/node_modules/mongoose/lib/query.js:4357:16)
at run (/script.js:26:25)
Note that you must explicitly call the Query#exec()
function,
using await Model.findOne()
isn't enough to get the full stack trace.
Moving On
Schema#pick()
and better server selection timeout errors are just 2 of
the 15 new features in Mongoose 5.8.0. Mongoose 5.8 also includes a
Model.validate()
function for validating POJOs, customizable CastError
messages,
and enum
for numbers. You can find the
full list on the Mongoose changelog. Make sure you upgrade to
take advantage of all the new features!
Want to become your team's MongoDB expert? "Mastering Mongoose" distills 8 years of hard-earned lessons building Mongoose apps at scale into 153 pages. That means you can learn what you need to know to build production-ready full-stack apps with Node.js and MongoDB in a few days. Get your copy!