Meituan-Dianping/mpvue · error

You may have an infinite update loop in watcher with express

Error message

You may have an infinite update loop in watcher with expression "${watcher.expression}" / in a component render function.

What it means

Dev-mode warning from the scheduler's flushSchedulerQueue when the same watcher is re-queued and run more than MAX_UPDATE_COUNT (100) times in one flush cycle. It indicates a reactive update loop: a watcher or render function mutates state that triggers itself again.

Source

Thrown at src/core/observer/scheduler.js:63

  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

View on GitHub (pinned to 6c5d78ee04)

Solutions

  1. Find the watcher expression named in the warning and remove its self-triggering write
  2. Move mutations out of render/computed getters into methods or lifecycle hooks
  3. Guard the watcher: only assign when the value actually changed
  4. Break parent-child cycles by emitting events upward instead of writing props back
  5. Use a debounce/nextTick for cascading updates

Example fix

// before
watch: { value(v) { this.value = v.trim() } } // re-triggers itself
// after
computed: {
  trimmed: {
    get() { return this.value },
    set(v) { this.$emit('input', v.trim()) }
  }
}
Defensive patterns

Strategy: validation

Validate before calling

function watcherWritesWatchedKey(watchers, computedKeys) {
  const offenders = []
  for (const [expr, def] of Object.entries(watchers || {})) {
    const handler = typeof def === 'function' ? def : def && def.handler
    if (handler && String(handler).includes('this.' + expr)) offenders.push(expr)
  }
  return offenders
}

Type guard

function isAcyclicWatcher(expr, handler) { return !String(handler).includes('this.' + expr) }

Prevention

When it happens

Trigger: A watcher whose handler sets the same value it watches; a computed that both reads and writes state consumed by the render; a component render function mutating reactive data used in the template (e.g. calling a method that increments during render); two watchers updating each other in a cycle.

Common situations: v-model on a computed without a setter guard; parent-child prop/event ping-pong where each side writes on change; watchers with deep:true on large mutating structures; side effects in computed getters.

Related errors


AI-assisted analysis of Meituan-Dianping/mpvue@6c5d78ee04 (2026-09-02). Data as JSON: /api/errors/f68931d2c5805809. Report an issue: GitHub.