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
- Update the caller so it never calls Add after Finish (finish the updater only once all stop packets for the wait are consumed)
- Make Add tolerant: return an error or ignore additions after done instead of panicking
- 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
- Finish the threadUpdater only after all stop packets of a wait are consumed
- Enable gdbserial logging (-log-output gdbserial) to diagnose late/interleaved stop packets
- Change Add after Finish to return an error instead of panicking in downstream patches
- Add race tests with multiple stop notifications during interrupt handling
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
- could not find thread %s
- not implemented
- unsupported operating system attempting to find Goroutine on
- selectThread when thread suffix is supported
- must specify at least one argument for qRRCmd
AI-assisted analysis of go-delve/delve@a23773e6c3 (2026-08-31).
Data as JSON: /api/errors/e7e8be193cc9f5be.
Report an issue: GitHub.