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 a MongoTimeoutError.

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!

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus