Automattic/mongoose · error · Error

Path '${path}' contains the same array filter multiple times

Error message

Path '${path}' contains the same array filter multiple times

What it means

MongoDB requires each array filter identifier to appear at most once per dotted update path: 'a.$[x].b.$[x]' is illegal because one identifier cannot carry two different filters within the same path. Mongoose computes the base path each identifier maps to (for versioning and discriminator handling in updatedPathsByArrayFilter) and throws this Error when the same identifier matches multiple times in one path.

Source

Thrown at lib/helpers/update/updatedPathsByArrayFilter.js:19

'use strict';

const modifiedPaths = require('./modifiedPaths');

module.exports = function updatedPathsByArrayFilter(update) {
  if (update == null) {
    return {};
  }
  const updatedPaths = modifiedPaths(update);

  return Object.keys(updatedPaths).reduce((cur, path) => {
    const matches = path.match(/\$\[[^\]]+\]/g);
    if (matches == null) {
      return cur;
    }
    for (const match of matches) {
      const firstMatch = path.indexOf(match);
      if (firstMatch !== path.lastIndexOf(match)) {
        throw new Error(`Path '${path}' contains the same array filter multiple times`);
      }
      cur[match.substring(2, match.length - 1)] = path.
        substring(0, firstMatch - 1).
        replace(/\$\[[^\]]+\]/g, '0');
    }
    return cur;
  }, {});
};

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Use a distinct identifier per array level: 'comments.$[c].replies.$[r].text' with arrayFilters: [{ 'c.approved': true }, { 'r.visible': true }]
  2. If both levels need the same condition, still use two identifiers and repeat the condition for each
  3. Split the operation into separate updateOne calls, one per array level, if a single path is unwieldy

Example fix

// before
await Model.updateOne({},
  { $set: { 'comments.$[c].replies.$[c].text': 'hi' } },
  { arrayFilters: [{ 'c.approved': true }] });

// after
await Model.updateOne({},
  { $set: { 'comments.$[c].replies.$[r].text': 'hi' } },
  { arrayFilters: [{ 'c.approved': true }, { 'r.visible': true }] });
Defensive patterns

Strategy: validation

Validate before calling

// Reject update paths that reuse an array filter identifier
function assertDistinctIdentifiers(update) {
  for (const op of Object.keys(update)) {
    for (const path of Object.keys(update[op] ?? {})) {
      const ids = path.match(/\$\[([^\]]+)\]/g) ?? [];
      const seen = new Set();
      for (const id of ids) {
        if (seen.has(id)) throw new Error(`Path '${path}' reuses identifier ${id}`);
        seen.add(id);
      }
    }
  }
}

Type guard

const hasUniqueFilterIds = (path) => { const ids = path.match(/\$\[([^\]]+)\]/g) ?? []; return new Set(ids).size === ids.length; };

Try / catch

try {
  await Model.updateOne(f, u, { arrayFilters });
} catch (err) {
  if (/same array filter multiple times/.test(err.message)) {
    // rename the inner identifier (e.g. $[c].$[c] -> $[c].$[r]) and add its filter, then retry
  } else throw err;
}

Prevention

When it happens

Trigger: Model.updateOne({}, { $set: { 'comments.$[c].replies.$[c].text': 'hi' } }, { arrayFilters: [{ 'c.approved': true }] }) — identifier 'c' reused for both the comments and the replies level.

Common situations: Nested array updates where the first identifier is reused for the inner array level; template/string-built update paths that concatenate '.$[x]' repeatedly; porting raw shell queries without adding a second identifier.

Related errors


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