mongoose
async-await
nodejs
mongodb
javascript

Mongoose async/await find then edit and save?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

With Mongoose, the normal async flow for "find, edit, then save" is to await the query, mutate the returned document, then await save(). That pattern is readable and works well when you need validation, middleware, or instance methods. The deeper question is whether you actually need a document round trip at all, or whether an atomic update query is the better fit.

Basic Find, Edit, Save Pattern

This is the standard document-oriented approach.

javascript
1const mongoose = require("mongoose");
2
3const userSchema = new mongoose.Schema({
4  name: String,
5  isActive: Boolean,
6  loginCount: Number,
7});
8
9const User = mongoose.model("User", userSchema);
10
11async function activateUser(userId) {
12  const user = await User.findById(userId);
13  if (!user) {
14    throw new Error("User not found");
15  }
16
17  user.isActive = true;
18  user.loginCount += 1;
19  await user.save();
20  return user;
21}

This is the right pattern when your business logic depends on working with a document instance in memory.

Error Handling With try And catch

Use try and catch when you want to add context or transform the error, not just because the function is async.

javascript
1async function renameUser(userId, newName) {
2  try {
3    const user = await User.findById(userId);
4    if (!user) {
5      throw new Error("User not found");
6    }
7
8    user.name = newName;
9    await user.save();
10    return user;
11  } catch (err) {
12    err.message = `renameUser failed: ${err.message}`;
13    throw err;
14  }
15}

That catch block is useful because it adds meaning. A catch block that only rethrows unchanged is usually unnecessary noise.

When findOneAndUpdate Is Better

If you do not need a live document instance, an atomic update is often safer and faster.

javascript
1async function activateUserAtomic(userId) {
2  const user = await User.findOneAndUpdate(
3    { _id: userId },
4    { $set: { isActive: true }, $inc: { loginCount: 1 } },
5    { new: true, runValidators: true }
6  );
7
8  if (!user) {
9    throw new Error("User not found");
10  }
11
12  return user;
13}

This avoids a race where another process changes the document between your read and your save.

Choose The Pattern Based On Behavior

Use find plus save when you need:

  • 'save() middleware,'
  • instance methods,
  • several dependent in-memory edits before persistence.

Use atomic update queries when you need:

  • fewer round trips,
  • update operators such as $inc, $push, or $set,
  • stronger protection against concurrent modification.

Neither pattern is always better. They solve slightly different problems.

Always Check For Missing Documents

A common bug is assuming the query always returns a document. In Mongoose, a miss returns null, so you must handle that before mutating fields or calling save().

That matters especially in request handlers where IDs come from the outside world and should not be trusted blindly.

Transactions For Multi-Document Consistency

If one user action modifies multiple documents or collections and must remain consistent, use a transaction with a session. Async syntax makes multi-step flows easier to read, but it does not make them atomic automatically.

That is a persistence design issue, not an async style issue.

Common Pitfalls

  • Calling save() without checking whether the query returned null.
  • Using fetch-then-save where an atomic update would be safer.
  • Wrapping every async function in pointless try and catch blocks.
  • Assuming findOneAndUpdate behaves exactly like save() middleware.
  • Ignoring concurrent write races in high-traffic code paths.

Summary

  • The normal Mongoose async pattern is await query, mutate the document, then await save().
  • Use that flow when you need document validation, middleware, or instance logic.
  • Prefer atomic update queries when concurrency and round-trip cost matter more.
  • Always handle the not-found case explicitly.
  • Choose the persistence pattern based on correctness requirements, not habit.

Course illustration
Course illustration

All Rights Reserved.