go-delve/delve · error
error while waiting after adding thread: %d %s
Error message
error while waiting after adding thread: %d %s
What it means
When PtraceSetOptions fails with ESRCH (thread not found — often because the stop notification from attach hasn't been consumed yet), Delve calls waitFast(tid) to settle the thread before retrying. This error means that recovery wait itself failed, so the new thread's ptrace options could not be configured.
Source
Thrown at pkg/proc/native/proc_linux.go:376
// Do not return err if err == EPERM,
// we may already be tracing this thread due to
// PTRACE_O_TRACECLONE. We will surely blow up later
// if we truly don't have permissions.
return nil, fmt.Errorf("could not attach to new thread %d %s", tid, err)
}
pid, status, err := dbp.waitFast(tid)
if err != nil {
return nil, err
}
if status.Exited() {
return nil, fmt.Errorf("thread already exited %d", pid)
}
}
dbp.execPtraceFunc(func() { err = syscall.PtraceSetOptions(tid, ptraceOptions) })
if err == syscall.ESRCH {
if _, _, err = dbp.waitFast(tid); err != nil {
return nil, fmt.Errorf("error while waiting after adding thread: %d %s", tid, err)
}
dbp.execPtraceFunc(func() { err = syscall.PtraceSetOptions(tid, ptraceOptions) })
if err == syscall.ESRCH {
return nil, err
}
if err != nil {
return nil, fmt.Errorf("could not set options for new traced thread %d %s", tid, err)
}
}
dbp.threads[tid] = &nativeThread{
ID: tid,
dbp: dbp,
os: new(osSpecificDetails),
}
if dbp.memthread == nil {
dbp.memthread = dbp.threads[tid]
}View on GitHub (pinned to a23773e6c3)
Solutions
- Retry the attach operation; the race is usually transient.
- Check that the target process is still alive and the TID still exists in /proc/<pid>/task/.
- If debugging via container runtimes, ensure the debugger runs in the same PID namespace and has CAP_SYS_PTRACE.
- Avoid attaching to processes currently being traced or reaped by another tool (strace, another dlv instance).
Defensive patterns
Strategy: retry
Validate before calling
// confirm the TID is still live before option setup
dead, err := os.Stat(fmt.Sprintf("/proc/%d/task/%d", pid, tid))
if err != nil || dead == nil {
return fmt.Errorf("thread %d vanished, rescan thread list", tid)
} Type guard
func isWaitAfterAddErr(err error) bool {
return err != nil && strings.Contains(err.Error(), "error while waiting after adding thread")
} Try / catch
err := updateThreadList()
if isWaitAfterAddErr(err) {
time.Sleep(10 * time.Millisecond)
err = updateThreadList() // retry once; races are usually transient
} Prevention
- Ensure the debugger shares the PID namespace with the target (same container or host PID namespace)
- Grant CAP_SYS_PTRACE in containerized environments
- Avoid concurrent tracers (strace + dlv) on the same process
- Retry thread-list refresh on transient failures
When it happens
Trigger: addThread: syscall.PtraceSetOptions returned ESRCH, then dbp.waitFast(tid) returned an error (e.g. ECHILD because the thread was reaped elsewhere or isn't a child, ESRCH because the thread vanished, EINTR not retried) while setting options for a newly attached thread.
Common situations: Attaching to processes with heavy thread creation/destruction where the TID disappears between attach and SetOptions; ptrace event wait races in multi-process (follow-exec) debugging; older kernels with non-standard wait semantics under containers.
Related errors
- thread already exited %d
- process must be stopped in order to kill it
- waiting for target execve failed: %s
- could not attach to new thread %d %s
- could not set options for new traced thread %d %s
AI-assisted analysis of go-delve/delve@a23773e6c3 (2026-08-31).
Data as JSON: /api/errors/24e8dd6d02ea0626.
Report an issue: GitHub.