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

  1. Retry the attach operation; the race is usually transient.
  2. Check that the target process is still alive and the TID still exists in /proc/<pid>/task/.
  3. If debugging via container runtimes, ensure the debugger runs in the same PID namespace and has CAP_SYS_PTRACE.
  4. 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

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


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