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
- Give each replacement handler a disjoint set of operators, or disjoint fieldTypes arrays, so at most one matches any resolved operator+fieldType.
- If both handlers legitimately target the same operator/fieldType, merge them into a single handler whose build branches on the field.
- 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
- Always set a narrow fieldTypes array on replacement handlers instead of leaving it undefined.
- Keep all operator handlers in one module so overlaps are visually obvious.
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
- Operator handler "${handler.name}" cannot define both "build
- Exceeded file size limit. Limit: ${bytesToMB(filesizeLimit).
- Invalid template given
- Invalid database type given. Valid types are: ${Object.value
- No version or tag "${version}" found for package: ${packageN
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/7deb37f0046a2462.
Report an issue: GitHub.