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 explain

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Remove the field-level index: true when a schema.index() already covers that field, or vice versa — keep exactly one declaration.
  2. 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.
  3. Run model.syncIndexes() and inspect the resulting list with db.collection.getIndexes() to confirm each intended index exists once.
  4. 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

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


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