Automattic/mongoose · warning · MongooseError
Invalid arg "${arg}" to unwind(), must be string or object
Error message
Invalid arg "${arg}" to unwind(), must be string or object What it means
When a model class is compiled (or loadClass() runs), applyMethods copies schema.methods onto the model prototype. If a method name is in schema.reserved (save, remove, populate, overwrite, deleteOne, ... — the same reserved list Document uses), overwriting it on the prototype can break Mongoose internals, so Mongoose warns. The warning can be suppressed per method with a third options argument. Self-referencing a built-in (schema.method('save', Document.prototype.save)) is silently deleted instead, to support classes extending Document.
Source
Thrown at lib/aggregate.js:467
* @see $unwind https://www.mongodb.com/docs/manual/reference/aggregation/unwind/
* @param {string|object|string[]|object[]} fields the field(s) to unwind, either as field names or as [objects with options](https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/#document-operand-with-options). If passing a string, prefixing the field name with '$' is optional. If passing an object, `path` must start with '$'.
* @return {Aggregate}
* @api public
*/
Aggregate.prototype.unwind = function() {
const args = [...arguments];
const res = [];
for (const arg of args) {
if (arg && typeof arg === 'object') {
res.push({ $unwind: arg });
} else if (typeof arg === 'string') {
res.push({
$unwind: (arg[0] === '$') ? arg : '$' + arg
});
} else {
throw new MongooseError('Invalid arg "' + arg + '" to unwind(), ' +
'must be string or object');
}
}
return this.append.apply(this, res);
};
/**
* Appends a new $replaceRoot operator to this aggregate pipeline.
*
* Note that the `$replaceRoot` operator requires field strings to start with '$'.
* If you are passing in a string Mongoose will prepend '$' if the specified field doesn't start '$'.
* If you are passing in an object the strings in your expression will not be altered.
*
* #### Example:
*
* aggregate.replaceRoot("user");
*View on GitHub (pinned to 49cdab0136)
Solutions
- Rename the custom method (e.g. save -> saveDraft, deleteOne -> archive) and keep built-ins intact.
- If you really intend to override, suppress explicitly: schema.method('save', fn, { suppressWarning: true }) — but test all internal call paths that rely on the original.
- For behavior to run around save/delete, prefer middleware: schema.pre('save', fn) / schema.post('findOneAndDelete', fn) instead of shadowing methods.
- With loadClass(), rename the class method or mark it static if it does not need an instance receiver.
Example fix
// before
schema.methods.save = function() { /* custom */ };
// after
schema.pre('save', function(next) { /* custom logic */ next(); });
// or, if overriding deliberately:
schema.method('save', function() { /* custom */ }, { suppressWarning: true }); Defensive patterns
Strategy: validation
Validate before calling
function addMethod(schema, name, fn) {
if (schema.reserved && schema.reserved[name]) {
throw new Error(`Method "${name}" is reserved by Mongoose; rename it or pass { suppressWarning: true }`);
}
schema.method(name, fn);
} Prevention
- Route all custom method registration through one helper that checks schema.reserved.
- Prefer middleware (pre/post hooks) over shadowing built-in methods.
- When using loadClass(), review class method names against the reserved list before import.
When it happens
Trigger: schema.method('save', fn), schema.methods.deleteOne = fn, class methods named 'populate'/'remove'/'save' on a class passed to schema.loadClass(); defining a static-like helper on methods whose name matches a Document method without the suppressWarning option.
Common situations: Domain models that naturally want customSave/deleteOne semantics; porting classes with existing method names into Mongoose via loadClass; teams discovering collisions only after upgrading when the reserved list grew; wrapping save with pre-hooks instead would be the idiomatic fix.
Related errors
- Aggregate `near()` must be called with non-nullish argument
- Invalid arg "${arg}" to sortByCount(), must be string or obj
- Invalid graphLookup() argument. Must be an object.
- Invalid sort() argument. Must be a string or object.
- If thenExpr or elseExpr is string, it must be either $$DESCE
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/f1063b7422c29245.
Report an issue: GitHub.