t8y2/dbx · error

ETCD_WATCH_NOT_FOUND

ETCD_WATCH_NOT_FOUND

Error message

ETCD_WATCH_NOT_FOUND: watch does not exist

What it means

watchPoll looks up the watch state by 'watchId' in the session's watch registry. If no watch with that ID exists, polling cannot proceed and this error is thrown. A watch may be absent because it was never started, already terminated (terminal watches are auto-removed in watchPoll), or belongs to a different session.

Source

Thrown at agents/drivers/etcd-go/watch.go:357

		row["metadata"] = metadataMap(item)
	}
	return row
}

func eventType(event *clientv3.Event) string {
	if event.Type == mvccpb.DELETE {
		return "delete"
	}
	return "put"
}

func (s *etcdSession) watchPoll(params map[string]json.RawMessage) (any, error) {
	watchID := stringOrDefault(params, "watchId", "")
	s.watchesMu.Lock()
	state := s.watches[watchID]
	s.watchesMu.Unlock()
	if state == nil {
		return nil, errors.New("ETCD_WATCH_NOT_FOUND: watch does not exist")
	}
	result := state.poll()
	if _, terminal := result["terminal"]; terminal {
		if removed := s.removeWatch(watchID); removed != nil {
			removed.close()
		}
	}
	return result, nil
}

func (s *etcdSession) watchStop(params map[string]json.RawMessage) (any, error) {
	if state := s.removeWatch(stringOrDefault(params, "watchId", "")); state != nil {
		state.close()
	}
	return map[string]bool{"stopped": true}, nil
}

View on GitHub (pinned to c0390bff16)

Solutions

  1. Use the watchId exactly as returned by watchStart
  2. Treat this error as terminal: stop polling and start a new watch via watchStart
  3. On session restart, discard old watch IDs and re-create watches
  4. Guard against polling the same watch from two consumers

Example fix

// before
result := poll(oldWatchID) // removed after terminal event
// after
if isWatchNotFound(err) { state = startWatch(key, prefix) }
Defensive patterns

Strategy: try-catch

Validate before calling

func (s *Session) hasWatch(id string) bool {
	s.watchesMu.Lock(); defer s.watchesMu.Unlock()
	return s.watches[id] != nil
}

Try / catch

res, err := agent.handle(ctx, "watchPoll", map[string]json.RawMessage{"watchId": idJSON})
if err != nil {
	if strings.Contains(err.Error(), "ETCD_WATCH_NOT_FOUND") {
		id = restartWatch(key, prefix) // recreate and resume
		return
	}
	return err
}

Prevention

When it happens

Trigger: Calling watchPoll with a watchId that was never returned by watchStart, reusing a watchId after the watch reached a terminal result (it is removed on first terminal poll), or polling a watch created in a different session.

Common situations: Clients caching watch IDs after restarts; double-consuming a terminal watch event; race where two pollers race and one removes the watch first; typo'd or truncated ID.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/7a3383f8ee2f98b0. Report an issue: GitHub.