go-delve/delve · error

threadUpdater: Add after Finish

Error message

threadUpdater: Add after Finish

What it means

threadUpdater tracks which gdbserial threads have been seen and reports new OS-thread-to-goroutine mappings. Its lifecycle is: Reset/Add(...)/Finish; after Finish() is called (done=true) the updater is considered complete and adding more threads is a protocol/state bug, so Add panics. In gdbserver.go the interrupt-and-wait flow can finish the updater and then receive another stop packet containing thread lists.

Source

Thrown at pkg/proc/gdbserial/gdbserver.go:1366

// FollowExec enables (or disables) follow exec mode
func (p *gdbProcess) FollowExec(bool) error {
	return errors.New("follow exec not supported")
}

type threadUpdater struct {
	p    *gdbProcess
	seen map[int]bool
	done bool
}

func (tu *threadUpdater) Reset() {
	tu.done = false
	tu.seen = nil
}

func (tu *threadUpdater) Add(threads []string) error {
	if tu.done {
		panic("threadUpdater: Add after Finish")
	}
	if tu.seen == nil {
		tu.seen = map[int]bool{}
	}
	for _, threadID := range threads {
		b := threadID
		if period := strings.Index(b, "."); period >= 0 {
			b = b[period+1:]
		}
		n, err := strconv.ParseUint(b, 16, 32)
		if err != nil {
			return &GdbMalformedThreadIDError{threadID}
		}
		tid := int(n)
		tu.seen[tid] = true
		if _, found := tu.p.threads[tid]; !found {
			tu.p.threads[tid] = &gdbThread{ID: tid, strID: threadID, p: tu.p}
		}

View on GitHub (pinned to a23773e6c3)

Solutions

  1. Update the caller so it never calls Add after Finish (finish the updater only once all stop packets for the wait are consumed)
  2. Make Add tolerant: return an error or ignore additions after done instead of panicking
  3. Capture verbose gdbserial logs (-log-output gdbserial) and reproduce to see which packet ordering triggers the late Add; then fix the wait loop

Example fix

// before
func (tu *threadUpdater) Add(threads []string) error {
	if tu.done {
		panic("threadUpdater: Add after Finish")
	}
// after
func (tu *threadUpdater) Add(threads []string) error {
	if tu.done {
		return errors.New("threadUpdater: Add after Finish")
	}
Defensive patterns

Strategy: try-catch

Validate before calling

// Go has no pre-call check; guard by checking updater state at call sites:
if tu.done {
	// late stop packet: skip Add instead of calling it after Finish
} else {
	if err := tu.Add(threads); err != nil { /* handle */ }
}

Try / catch

defer func() {
	if r := recover(); r != nil {
		if s, ok := r.(string); ok && strings.Contains(s, "threadUpdater: Add after Finish") {
			// late stop packet: ignore thread additions
			return
		}
		panic(r)
	}
}()

Prevention

When it happens

Trigger: Calling Add after Finish on a threadUpdater — e.g. when the gdbserial target delivers an additional stop/reply packet containing a thread list after the wait loop already finished the updater (waitTimeout paths, multiple stops racing during interrupt).

Common situations: Racing stop notifications from a remote target (lldb-server/debugserver/rr) delivering thread info late; timeouts in waitForStop causing Finish, followed by a late thread list; flaky remote debugging sessions over slow connections.

Related errors


AI-assisted analysis of go-delve/delve@a23773e6c3 (2026-08-31). Data as JSON: /api/errors/e7e8be193cc9f5be. Report an issue: GitHub.