payloadcms/payload · critical · APIError

Operator handlers "${handlerA.name}" and "${handlerB.name}"

Error message

Operator handlers "${handlerA.name}" and "${handlerB.name}" both replace the "${sharedOperators.join('", "')}" operator for overlapping field types. Only one replacement handler may match a given resolved operator and field type.

What it means

Thrown at adapter boot when two replacement operator handlers (handlers with a 'build' function) both declare the same operator AND their fieldTypes overlap. Because a replacement handler fully replaces the comparison, two of them matching the same resolved (operator, fieldType) would be non-deterministic, so the adapter refuses to start. Note fieldTypesOverlap treats undefined fieldTypes as 'matches everything'.

Source

Thrown at packages/drizzle/src/queries/validateOperatorHandlers.ts:47

  }

  const replacementHandlers = operatorHandlers.filter(isReplacementHandler)

  for (let i = 0; i < replacementHandlers.length; i++) {
    for (let j = i + 1; j < replacementHandlers.length; j++) {
      const handlerA = replacementHandlers[i]
      const handlerB = replacementHandlers[j]

      const sharedOperators = handlerA.operators.filter((operator) =>
        handlerB.operators.includes(operator),
      )

      if (!sharedOperators.length) {
        continue
      }

      if (fieldTypesOverlap(handlerA, handlerB)) {
        throw new APIError(
          `Operator handlers "${handlerA.name}" and "${handlerB.name}" both replace the "${sharedOperators.join('", "')}" operator for overlapping field types. Only one replacement handler may match a given resolved operator and field type.`,
        )
      }
    }
  }
}

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Give each replacement handler a disjoint set of operators, or disjoint fieldTypes arrays, so at most one matches any resolved operator+fieldType.
  2. If both handlers legitimately target the same operator/fieldType, merge them into a single handler whose build branches on the field.
  3. Drop one of the conflicting handlers if it is redundant.

Example fix

// before
const a = { name: 'a', operators: ['like'], fieldTypes: ['text'], build: ... }
const b = { name: 'b', operators: ['like'], fieldTypes: ['text'], build: ... }
// after
const b = { name: 'b', operators: ['like'], fieldTypes: ['textarea'], build: ... }
Defensive patterns

Strategy: validation

Validate before calling

function assertNoReplacementOverlap(handlers) {
  const reps = handlers.filter(h => typeof h.build === 'function')
  for (let i = 0; i < reps.length; i++) {
    for (let j = i + 1; j < reps.length; j++) {
      const a = reps[i], b = reps[j]
      const sharedOps = a.operators.filter(o => b.operators.includes(o))
      const typesOverlap = !a.fieldTypes || !b.fieldTypes || a.fieldTypes.some(t => b.fieldTypes.includes(t))
      if (sharedOps.length && typesOverlap) {
        throw new Error(`${a.name} and ${b.name} overlap on ${sharedOps}`)
      }
    }
  }
}

Type guard

const isReplacement = (h) => typeof h.build === 'function'

Prevention

When it happens

Trigger: Registering two operatorHandlers that both list (for example) 'like' in their `operators` array, where both omit fieldTypes or both include a common fieldType such as 'text'.

Common situations: Adding a plugin that contributes its own operator handlers that collide with handlers you already registered; forking a handler and forgetting to narrow its fieldTypes or rename its operator.

Related errors


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/7deb37f0046a2462. Report an issue: GitHub.