Client: Surprising difference between update with callback and updateAsync

When investigating `Mongo.Collection` calling wrong allow/deny methods · Issue #13444 · meteor/meteor · GitHub we noticed a quite surprising behaviour in Meteor 2 and Meteor 3.

The standard behaviour in JS for any method that takes a callback method as the last argument is that it can be easily “promisified” into an async method.

That used to be the way to get async versions for any meteor callback-based method and we also expect that any newly introduced async version of the same method should just be the same as the “promisified” version.

However, for the collection modification methods this is not true when running in the browser (client).

If we have collection modifications blocked on the server, this code will receive an error in the callback (as expected)

Meteor.users.update({},{$set:{field:12}},undefined,(err,numChanged)=>{ console.log({err,numChanged}});

However - this code will print 1 (numChanged) and not see an error, i.e. the “optimistic” answer before receiving a response from the server.

console.log(await Meteor.users.updateAsync({},{$set:{field:12}});

Adding the callback to the async method (which is weird) makes it work as before.

If this is indeed the expected behaviour it should be documented in big letters :slight_smile:

On the server, everything works as expected since there is no optimistic update thingy.

1 Like

We’ve discussed these differences internally and provided documentation to explain them. There might still be some missing details, but here’s a quick overview:

How Mongo Operations Differ: Async vs. Sync

  • Sync Version/Callback-based: Runs the MongoDB operation in the background via a server method call while simulating the result locally, returning optimistic data immediately.
const id = MyCollection.insert({ field:12 });
console.log(id) // 🧾 Data is available (Optimistic-UI)

// 🔴 Server ended with error (denied insert rule)
MyCollection.findOne({}); // 🗑️ Data is NOT available
  • Async Version: Runs the MongoDB operation in the background via a server method call, simulates the result locally, and returns promises (stubPromise, serverPromise) for handling the async flow.
const { stubPromise, serverPromise } = MyCollection.insertAsync({ field:12 });

const id = await stubPromise;

// 🔵 Client simulation
MyCollection.findOne({}); // 🧾 Data is available (Optimistic-UI)
console.log(id) // 🧾 Data is available (Optimistic-UI)

try {
  await serverPromise;
  // 🟢 Server ended with success
} catch(e) {
  console.error("Error:", error.reason); // 🔴 Server ended with error
}

MyCollection.findOne({}); // 🗑️ Data is NOT available

The benefits of the sync version are great for many client-side code, since usually handling async as part of your code can produce more complex code. And if you wish to enforce isomorphism as much as possible on client and server you have available the async version.

Since MongoDB operations are essentially Meteor methods under the hood, they share the same benefits, and are documented on Meteor.callAsync section. The documentation of MongoDB operations cover the interface of sync/async versions.

Do you have suggestions to refine these explanations further?

1 Like

I haven’t browsed through the entire documentation, but it must be linked on each of the affected functions. For example, I am in a hurry and wanted to know how updsteAsync works. Would the docs about updateAsync tell me about this? (This is just an example)

1 Like