There's some confusion on the internet about what happens when you call Model.find() in Mongoose. Make no mistake, Model.find() does what you expect: find all documents that match a query. But there's some confusion about Model.find() vs Query#find() , setting options, promise support. In this article, I'll provide a conceptual overview of what happens when you call Model.find() so you can answer similar questions for yourself.

Setup

For the purposes of this article, I'll assume you already have a MongoDB instance running on localhost:27017 . If you don't, check out run-rs, it downloads and runs MongoDB for you with no dependencies beyond Node.js. Here's a standalone script that demonstrates creating some documents and using find() :

const mongoose = require ( 'mongoose' ); run().catch(error => console .log(error.stack)); async function run ( ) { await mongoose.connect( 'mongodb://localhost:27017/test' , { useNewUrlParser: true }); await mongoose.connection.dropDatabase(); const customerSchema = new mongoose.Schema({ name: String , age: Number , email: String }); const Customer = mongoose.model( 'Customer' , customerSchema); await Customer.create({ name: 'A' , age: 30 , email: 'a@foo.bar' }); await Customer.create({ name: 'B' , age: 28 , email: 'b@foo.bar' }); const docs = await Customer.find(); console .log(docs); }

Models and Queries

Mongoose actually has two find() functions. The above standalone example uses Model.find() , but there's also Query#find() . Query#find() is shorthand for Query.prototype.find() , find() is an instance method on the Query class.

The Model.find() function returns an instance of Mongoose's Query class. The Query class represents a raw CRUD operation that you may send to MongoDB. It provides a chainable interface for building up more sophisticated queries. You don't instantiate a Query directly, Customer.find() instantiates one for you.

const query = Customer.find(); query instanceof mongoose.Query; const docs = await query;

So Model.find() returns an instance of the Query class. You can chain find() calls to add additional query operators, also known as filters, to the query. For example, both of the following queries will find all customers whose email contains 'foo.bar' and whose age is at least 30.

Customer.find({ email: /foo\.bar/ , age: { $gte: 30 } }); Customer.find({ email: /foo\.bar/ }).find({ age: { $gte: 30 } });

Query objects have numerous helpers for building up sophisticated CRUD operations. The most commonly used ones are Query#sort() , Query#limit() , and Query#skip() .

const res = await Customer.find({}).sort({ name: 1 }).limit( 1 ); const res2 = await Customer.find({}).sort({ name: 1 }).skip( 1 ).limit( 1 );

One major advantage of using Mongoose is that Mongoose casts queries to match the model's schema. This means you don't explicitly need to convert strings to ObjectIds or worry about the nuances of converting strings to numbers.

Customer.find({ _id: res[ 0 ]._id.toHexString(), age: { $gte: '25' } });

Setting Options

The sort() , limit() , and skip() helpers modify the query's options. For example, query.getOptions() below will return an object that contains sort and limit properties.

const query = Customer.find().sort({ name: 1 }).limit( 1 ); query.getOptions();

The Model.find() function takes 3 arguments that help you initialize a query without chaining. The first argument is the query filter (also known as conditions ). The 2nd argument is the query projection, which defines what fields to include or exclude from the query. For example, if you want to exclude the customer's email for privacy concerns, you can use either of the below syntaxes.

Customer.find({}, { email: 0 }); Customer.find().select({ email: 0 });

The 3rd argument to Model.find() is the general query options. Here's a full list of options. For example, you can set limit and skip in the 3rd argument.

const res = await Customer.find({}, null , { sort: { name: 1 }, limit: 1 }); res[ 0 ].name;

Note that Mongoose's Model.find() has a different function signature than the MongoDB driver's collection.find() function. The MongoDB driver only takes 2 arguments, filter and options . To convert a MongoDB driver find() call to a Mongoose Model.find() call without chaining, add null as the 2nd argument.

client.db().collection( 'customers' ).find({ email: /foo\.bar/ }, { limit: 1 }); Customer.find({ email: /foo\.bar/ }, null , { limit: 1 }); Customer.find({ email: /foo\.bar/ }).limit( 1 );

Promises and Async/Await

Model.find() returns a query instance, so why can you do await Model.find() ? That's because a Mongoose query is a thenable, meaning they have a then() function. That means you can use queries in the same way you use promises, including with promise chaining as shown below.

Customer.find({ name: 'A' }). then(customers => { console .log(customers[ 0 ].name); return Customer.find({ name: 'B' }); }). then(customers => { console .log(customers[ 0 ].name); });

Queries also have a catch() function. In general, a thenable doesn't need to have a catch() function, but Mongoose added one for your convenience. Below is an example of using catch() to handle a malformed number in your query.

Customer.find({ age: 'not a number' }). catch (err => console .log( 'Caught:' , err.message));

Queries are thenables, but queries are not promises. In some cases, you might need a promise, not just a thenable. For example, you may have strict TypeScript bindings or you may be using the cls-hooked module. The Query#exec() function returns a fully fledged promise.

const q = Customer.find(); q instanceof Promise ; q.exec() instanceof Promise ;

Moving On

Finding all documents that match a query in Mongoose is intuitive, but there's nuances that pop up once you go beyond the most basic queries. Mongoose lets you structure queries using chaining or, equivalently, using POJOs in a single function call. Model.find() returns a query, which has a separate find() method that lets you attach additional filters. Queries are not promises, but close enough for most practical uses. Remember these 3 concepts and you'll know enough to address most common Mongoose issues.

Mongoose supports async/await, but its common companion Express surprisingly doesn't. Want to know how to determine whether your favorite npm modules support async/await without reconciling contradictory answers on StackOverflow? Chapter 4 of Mastering Async/Await explains the core principles for determining whether a given library or framework supports async/await, so get the ebook today!