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 is called with a watchId that has no entry in the session's watches map, so there is nothing to poll. This happens when the watch was never started, already terminated (terminal results remove the watch), or the session was reused after the watch was cleaned up. The library deletes watches from its map once they return a terminal event, making subsequent polls invalid.
Source
Thrown at agents/drivers/etcd2-go/watch.go:379
if action == "delete" || action == "expire" || action == "compareAndDelete" {
return "delete"
}
return "put"
}
func (w *watchState) hasTerminal() bool {
w.mu.Lock()
defer w.mu.Unlock()
return w.terminalReason != ""
}
func (s *etcd2Session) 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 *etcd2Session) 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
- Treat the watch as ended: re-create it with watchStart and use the fresh watchId.
- Stop polling after any poll result containing 'terminal' — the library removes the watch automatically.
- Check that the watchId you pass is the exact id returned by watchStart from the same session.
- Guard against empty watchId: only poll when the start call succeeded and you persisted its id.
Example fix
// before
res, _ := session.watchPoll(map[string]any{"watchId": cachedID})
// after
res, err := session.watchPoll(map[string]any{"watchId": currentID})
if err != nil {
currentID = restartWatch(params) // watch no longer exists; recreate it
} Defensive patterns
Strategy: retry
Validate before calling
func canPoll(id string) error {
if id == "" { return errors.New("empty watchId") }
return nil
} Type guard
func hasWatchID(params map[string]json.RawMessage) bool {
var id string
return json.Unmarshal(params["watchId"], &id) == nil && id != ""
} Try / catch
res, err := session.watchPoll(params)
if err != nil && strings.Contains(err.Error(), "ETCD_WATCH_NOT_FOUND") {
currentID, err = session.watchStart(watchParams) // recreate and retry
} Prevention
- Stop polling once a result contains "terminal"
- Never cache watchIds across session restarts
- Always persist the id returned by watchStart immediately
When it happens
Trigger: Polling a watchId after a previous poll returned a terminal result; polling an id from a different or restarted session; passing an empty watchId (default of stringOrDefault) when the caller never stored the id returned by watchStart; a race where two pollers hit the same watch and one removes it.
Common situations: Client code caching watch ids across reconnects, retry loops that keep polling after the watch ended, and single-shot pollers that do not expect watch lifecycle termination.
Related errors
- ETCD_WATCH_NOT_FOUND
- Producer is not initialized. Call connect first.
- Not connected
- Agent session not found: <sessionId>
- Not connected
AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05).
Data as JSON: /api/errors/5986b57dd6dcbe84.
Report an issue: GitHub.