Automattic/mongoose · error · MongooseError

Document deleteOne pre hooks cannot overwrite arguments

Error message

Document deleteOne pre hooks cannot overwrite arguments

What it means

Document-style deleteOne middleware (pre('deleteOne', { document: true, query: false })) runs inside Mongoose's wrapper with fixed arguments (doc, options). If the pre hook replaces those arguments — classically by calling next() with values, e.g. next(null, otherDoc, otherOptions), or via a plugin that proxies and forwards replaced args — Mongoose throws this MongooseError, because its wrapper must execute with the original document and options.

Source

Thrown at lib/model.js:848

  const self = this;
  const where = this.$__where();
  const query = self.constructor.deleteOne();

  if (this.$session() != null) {
    if (!('session' in query.options)) {
      query.options.session = this.$session();
    }
  }

  const preFilter = buildMiddlewareFilter(options, 'pre');
  const postFilter = buildMiddlewareFilter(options, 'post');

  query.pre(async function queryPreDeleteOne() {
    const res = await self.constructor._middleware.execPre('deleteOne', self, [self, options], { filter: preFilter });
    // `self` is passed to pre hooks as argument for backwards compatibility, but that
    // isn't the actual arguments passed to the wrapped function.
    if (res[0] !== self || res[1] !== options) {
      throw new MongooseError('Document deleteOne pre hooks cannot overwrite arguments');
    }
    query.deleteOne(where, options);
    // Apply custom where conditions _after_ document deleteOne middleware for
    // consistency with save() - sharding plugin needs to set $where
    if (self.$where != null) {
      this.where(self.$where);
    }
    return res;
  });
  query.pre(function callSubdocPreHooks() {
    return Promise.all(self.$getAllSubdocs().map(subdoc => subdoc.constructor._middleware.execPre('deleteOne', subdoc, [subdoc], { filter: preFilter })));
  });
  query.pre(function skipIfAlreadyDeleted() {
    if (self.$__.isDeleted) {
      throw new Kareem.skipWrappedFunction();
    }
  });
  query.post(function callSubdocPostHooks() {

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Call next() with no arguments (or write async hooks with no return value) in document deleteOne pre hooks
  2. Modify behavior through `this` (the document) instead of replacing arguments
  3. Apply custom conditions via this.$where, or use query-level hooks (pre('deleteOne', { query: true, document: false })) and shape the Query

Example fix

// before
schema.pre('deleteOne', { document: true, query: false }, function (next) {
  next(null, this, { soft: true }); // replaces (doc, options) -> throws
});

// after
schema.pre('deleteOne', { document: true, query: false }, async function () {
  this.deletedAt = new Date(); // mutate the document instead
});
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await doc.deleteOne();
} catch (err) {
  if (err instanceof mongoose.MongooseError && /pre hooks cannot overwrite arguments/.test(err.message)) {
    // audit registered pre('deleteOne', { document: true }) hooks and plugins:
    // ensure they call next() with no arguments and mutate `this` instead
  } else throw err;
}

Prevention

When it happens

Trigger: A schema hook like schema.pre('deleteOne', { document: true, query: false }, function (next) { next(null, this, {}); }) followed by doc.deleteOne(); also plugins (soft-delete, audit trails) that wrap deleteOne middleware and pass replaced arguments through.

Common situations: Writing hooks with other libraries' semantics where next() carries results; porting query-middleware patterns that reshape arguments to document middleware; plugin interop layers around deleteOne.

Related errors


AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21). Data as JSON: /api/errors/66dd1203846d44e3. Report an issue: GitHub.