MongoDBfind() & findOne() Deep Dive

find() and findOne()

find() and findOne() are the core read methods. find() returns a cursor over every matching document; findOne() returns a single document (or null) directly, no cursor involved. This page goes deep on the filter/projection shape and the findOneAnd* family of atomic read-and-modify operations.

The Filter Document

The first argument to both methods is the filter — a document describing which records to match. An empty filter ({}, or omitted) matches every document.

Filter shapes

JS
db.users.find()                             // everything
db.users.find({ status: "active" })         // simple equality
db.users.find({ age: { $gte: 18 } })        // operator expression
db.users.find({ status: "active", age: { $gte: 18 } })   // implicit AND across fields
Projection — Shaping the Output

The second argument is the projection — which fields to return. You can list fields to include or fields to exclude, but (with one exception) not both in the same projection.

Include vs exclude projection

JS
// Inclusion — only name and email come back (plus _id, unless excluded)
db.users.find({}, { name: 1, email: 1 })

// Exclusion — everything EXCEPT passwordHash comes back
db.users.find({}, { passwordHash: 0 })

// Mixing inclusion and exclusion of NON-_id fields is an error:
db.users.find({}, { name: 1, email: 0 })   // ⚠ throws — can't mix, except for _id
The _id Exception

_id is the one field you can exclude alongside an otherwise-inclusion projection (or include alongside an otherwise-exclusion projection), because it's returned by default regardless of the projection's overall mode.

Excluding _id from an inclusion projection

JS
db.users.find({}, { name: 1, email: 1, _id: 0 })
// returns ONLY { name, email } — no _id at all
Chaining Cursor Methods

find() chained with cursor methods

JS
db.products
  .find({ category: "widgets" }, { name: 1, price: 1 })
  .sort({ price: -1 })
  .limit(10)
Common Query Shapes

Goal

Filter

Equality

{ status: "active" }

Range

{ price: { $gte: 10, $lte: 50 } }

One of several values

{ category: { $in: ["a", "b"] } }

Field exists

{ deletedAt: { $exists: false } }

Nested field

{ "address.city": "Toronto" }

Array contains value

{ tags: "featured" }

findOne()

findOne() is sugar for find(filter).limit(1) that also unwraps the cursor for you — it returns the matched document directly (or null if nothing matched), never a cursor.

findOne basics

JS
const user = db.users.findOne({ email: "alice@example.com" })
if (user === null) {
  // no match
}
findOneAndDelete()

Atomically find and delete, returning the deleted document

JS
const removed = db.tasks.findOneAndDelete({ status: "cancelled" })
// removed is the deleted document itself (or null) — useful for
// "claim and remove a job from a queue" style patterns
findOneAndReplace()

Atomically find and replace the whole document

JS
const before = db.users.findOneAndReplace(
  { email: "alice@example.com" },
  { email: "alice@example.com", name: "Alice Chen", age: 30 }
)
// by default returns the document as it was BEFORE the replace
findOneAndUpdate() and returnDocument

The most commonly used of the three — atomically applies an update and gives you the document back in the same round-trip, avoiding a separate read-modify-write race condition.

returnDocument: 'before' vs 'after'

JS
// Default — returns the document BEFORE the update was applied
db.counters.findOneAndUpdate(
  { _id: "orderNumber" },
  { $inc: { seq: 1 } }
)

// returnDocument: "after" — returns the document AFTER the update
db.counters.findOneAndUpdate(
  { _id: "orderNumber" },
  { $inc: { seq: 1 } },
  { returnDocument: "after" }
)
Tip
findOneAndUpdate() with { upsert: true, returnDocument: "after" } is the idiomatic way to implement an atomic "get or create" — for example, an auto-incrementing counter document, without a race condition between separate find and update calls.
Warning
The findOneAnd* family only ever affects one document, and only guarantees atomicity for that single document — it is not a substitute for a multi-document transaction when you need several documents changed together.
Example Session
test> db.users.findOne({ email: "alice@example.com" }, { name: 1, _id: 0 })
{ name: 'Alice Chen' }

test> db.counters.findOneAndUpdate(
...   { _id: "orderNumber" },
...   { $inc: { seq: 1 } },
...   { upsert: true, returnDocument: "after" }
... )
{ _id: 'orderNumber', seq: 1 }
  • find() → cursor; findOne() → a single document or null.

  • Projections are include-only or exclude-only, except _id which can mix with either mode.

  • findOneAndDelete/Replace/Update() combine a read and a write atomically, returning the document in the same round-trip.

  • returnDocument: "after" on findOneAndUpdate() is the standard pattern for atomic get-or-create counters and similar workflows.