go-delve/delve · error
thread already exited %d
Error message
thread already exited %d
What it means
After successfully ptrace-attaching to a new thread and calling waitpid on it, Delve found the thread's wait status reports it has already exited. The thread died in the window between attach and the wait, so it cannot be added to the debugger's thread list.
Source
Thrown at pkg/proc/native/proc_linux.go:369
ptraceOptions = ptraceOptionsFollowExec
}
var err error
if attach {
dbp.execPtraceFunc(func() { err = sys.PtraceAttach(tid) })
if err != nil && err != sys.EPERM {
// 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{View on GitHub (pinned to a23773e6c3)
Solutions
- Retry the operation (re-run updateThreadList); the dead thread will simply be absent from /proc on the next scan.
- Treat this as non-fatal in the caller: skip the TID and continue adding remaining threads instead of aborting the whole attach.
- Attach to a quiescent process: pause or reduce thread churn in the target if you control it.
- Attach earlier in the target's lifecycle (before heavy thread churn) rather than mid-shutdown.
Example fix
// before: any addThread error aborts the scan
if _, err := dbp.addThread(tid, tid != dbp.pid); err != nil {
return err
}
// after: skip threads that exited between glob and attach
if _, err := dbp.addThread(tid, tid != dbp.pid); err != nil {
if strings.Contains(err.Error(), "thread already exited") {
continue
}
return err
} Defensive patterns
Strategy: validation
Validate before calling
// verify the thread still exists before operating on it
func threadExists(pid, tid int) bool {
_, err := os.Stat(fmt.Sprintf("/proc/%d/task/%d", pid, tid))
return err == nil
} Type guard
func isThreadExitedErr(err error) bool {
return err != nil && strings.Contains(err.Error(), "thread already exited")
} Try / catch
if _, err := dbp.addThread(tid, true); err != nil {
if isThreadExitedErr(err) {
continue // benign race, thread is gone
}
return err
} Prevention
- Treat 'thread already exited' as benign when scanning /proc for threads — it's an inherent race
- Attach to quiescent processes when possible; avoid mid-shutdown targets
- Re-scan the thread list instead of retrying the dead TID
- Attach as early in the target's lifecycle as possible
When it happens
Trigger: addThread(tid, attach=true): sys.PtraceAttach succeeded (or returned EPERM which was tolerated), dbp.waitFast(tid) returned a status whose Exited() is true — i.e. the TID ceased to exist right after attach; typically happens when enumerating /proc/<pid>/task/* for a process with rapidly exiting threads.
Common situations: dlv attach to a busy server process spawning/destroying threads (thread pools resizing); attaching to a process that is itself shutting down; Go programs with many short-lived goroutine-backed OS threads exiting during scan.
Related errors
- could not attach to new thread %d %s
- error while waiting after adding thread: %d %s
- process must be stopped in order to kill it
- waiting for target execve failed: %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/68d7722ae0c1dc91.
Report an issue: GitHub.