{"record":{"id":"68d7722ae0c1dc91","repo":"go-delve/delve","slug":"thread-already-exited-d","errorCode":null,"errorMessage":"thread already exited %d","messagePattern":"thread already exited (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/proc/native/proc_linux.go","lineNumber":369,"sourceCode":"\t\tptraceOptions = ptraceOptionsFollowExec\n\t}\n\n\tvar err error\n\tif attach {\n\t\tdbp.execPtraceFunc(func() { err = sys.PtraceAttach(tid) })\n\t\tif err != nil && err != sys.EPERM {\n\t\t\t// Do not return err if err == EPERM,\n\t\t\t// we may already be tracing this thread due to\n\t\t\t// PTRACE_O_TRACECLONE. We will surely blow up later\n\t\t\t// if we truly don't have permissions.\n\t\t\treturn nil, fmt.Errorf(\"could not attach to new thread %d %s\", tid, err)\n\t\t}\n\t\tpid, status, err := dbp.waitFast(tid)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif status.Exited() {\n\t\t\treturn nil, fmt.Errorf(\"thread already exited %d\", pid)\n\t\t}\n\t}\n\n\tdbp.execPtraceFunc(func() { err = syscall.PtraceSetOptions(tid, ptraceOptions) })\n\tif err == syscall.ESRCH {\n\t\tif _, _, err = dbp.waitFast(tid); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error while waiting after adding thread: %d %s\", tid, err)\n\t\t}\n\t\tdbp.execPtraceFunc(func() { err = syscall.PtraceSetOptions(tid, ptraceOptions) })\n\t\tif err == syscall.ESRCH {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not set options for new traced thread %d %s\", tid, err)\n\t\t}\n\t}\n\n\tdbp.threads[tid] = &nativeThread{","sourceCodeStart":351,"sourceCodeEnd":387,"githubUrl":"https://github.com/go-delve/delve/blob/a23773e6c31361e43246bc43a424ee009679b174/pkg/proc/native/proc_linux.go#L351-L387","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: any addThread error aborts the scan\nif _, err := dbp.addThread(tid, tid != dbp.pid); err != nil {\n    return err\n}\n// after: skip threads that exited between glob and attach\nif _, err := dbp.addThread(tid, tid != dbp.pid); err != nil {\n    if strings.Contains(err.Error(), \"thread already exited\") {\n        continue\n    }\n    return err\n}","handlingStrategy":"validation","validationCode":"// verify the thread still exists before operating on it\nfunc threadExists(pid, tid int) bool {\n    _, err := os.Stat(fmt.Sprintf(\"/proc/%d/task/%d\", pid, tid))\n    return err == nil\n}","typeGuard":"func isThreadExitedErr(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"thread already exited\")\n}","tryCatchPattern":"if _, err := dbp.addThread(tid, true); err != nil {\n    if isThreadExitedErr(err) {\n        continue // benign race, thread is gone\n    }\n    return err\n}","preventionTips":["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"],"tags":["linux","ptrace","race-condition","threading"],"backgroundTag":"thread-exited-race","analyzedSha":"a23773e6c31361e43246bc43a424ee009679b174","analyzedAt":"2026-08-31T15:12:45.221Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}