Automattic/mongoose · warning · MongooseError
If thenExpr or elseExpr is string, it must be either $$DESCE
Error message
If thenExpr or elseExpr is string, it must be either $$DESCEND, $$PRUNE or $$KEEP
What it means
During index creation (gh-15056 fix), Mongoose compares each unnamed index spec against specs it has already seen using isIndexSpecEqual(). Two definitions covering the same keys with the same direction (e.g. { email: 1 } declared twice) usually mean the field was marked index: true and also declared with schema.index({ email: 1 }). The duplicate is not harmful to correctness but creates redundant createIndex attempts and log noise, so Mongoose warns and asks you to remove one.
Source
Thrown at lib/aggregate.js:769
* }
* });
*
* // $redact often comes with $cond operator, you can also use the following syntax provided by mongoose
* await Model.aggregate(pipeline).redact({ $eq: [ '$level', 5 ] }, '$$PRUNE', '$$DESCEND');
*
* @param {object} expression redact options or conditional expression
* @param {string|object} [thenExpr] true case for the condition
* @param {string|object} [elseExpr] false case for the condition
* @return {Aggregate} this
* @see $redact https://www.mongodb.com/docs/manual/reference/operator/aggregation/redact/
* @api public
*/
Aggregate.prototype.redact = function(expression, thenExpr, elseExpr) {
if (arguments.length === 3) {
if ((typeof thenExpr === 'string' && !validRedactStringValues.has(thenExpr)) ||
(typeof elseExpr === 'string' && !validRedactStringValues.has(elseExpr))) {
throw new MongooseError('If thenExpr or elseExpr is string, it must be either $$DESCEND, $$PRUNE or $$KEEP');
}
expression = {
$cond: {
if: expression,
then: thenExpr,
else: elseExpr
}
};
} else if (arguments.length !== 1) {
throw new TypeError('Invalid arguments');
}
return this.append({ $redact: expression });
};
/**
* Execute the aggregation with explainView on GitHub (pinned to 49cdab0136)
Solutions
- Remove the field-level index: true when a schema.index() already covers that field, or vice versa — keep exactly one declaration.
- If both are needed conceptually (single-field plus different compound), give them distinct names so they are not 'duplicate unnamed' specs, and verify the compound actually differs in keys/direction.
- Run model.syncIndexes() and inspect the resulting list with db.collection.getIndexes() to confirm each intended index exists once.
- Codemod your schemas to a single style (prefer schema.index()) to prevent future duplicates.
Example fix
// before
const schema = new Schema({ email: { type: String, index: true } });
schema.index({ email: 1 }); // duplicate
// after
const schema = new Schema({ email: String });
schema.index({ email: 1 }); Defensive patterns
Strategy: validation
Validate before calling
const specKey = (fields) => JSON.stringify(Object.keys(fields).sort().map(k => [k, fields[k]]));
function assertNoDuplicateIndexes(schema) {
const seen = new Set();
for (const [fields, options] of schema.indexes()) {
if (options.name == null) {
const key = specKey(fields);
if (seen.has(key)) throw new Error(`Duplicate index on ${key}; keep either index:true or schema.index()`);
seen.add(key);
}
}
} Prevention
- Pick one indexing style per codebase (prefer schema.index()) and lint against index: true.
- After adding a compound index, remove now-redundant single-field index: true on its prefix fields when the single index is unnecessary.
- Run model.syncIndexes() in staging and diff getIndexes() output before shipping schema changes.
When it happens
Trigger: new Schema({ email: { type: String, index: true } }) combined with schema.index({ email: 1 }); compound indexes where one field also has index: true (e.g. { email: 1 } from index:true and schema.index({ email: 1, name: -1 }) — note same-key single-field duplicates are what triggers the exact-match warning); copied index blocks that declare the same spec twice; both unique: true on the field and a schema.index with the same keys.
Common situations: Growing schemas where a field first got index: true and later a compound index was added; merging PRs where two devs index the same field; importing index definitions from db.collection.getIndexes() output that already includes field-level indexes; performance cleanups after syncIndexes() reports duplicate work.
Related errors
- Invalid sort() argument. Must be a string or object.
- Aggregate `near()` must be called with non-nullish argument
- Invalid arg "${arg}" to unwind(), must be string or object
- Invalid arg "${arg}" to sortByCount(), must be string or obj
- Invalid graphLookup() argument. Must be an object.
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/86c2c1430a359649.
Report an issue: GitHub.