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.

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.

Appendix: 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)