go-delve/delve · error

thread %d does not exist

Error message

thread %d does not exist

What it means

Target.SwitchThread was asked to make thread `tid` the current thread, but no thread with that ID exists in the target process. Delve only allows switching to threads it is actively tracking; when a thread has exited (or never existed) it refuses the switch and returns this error.

Source

Thrown at pkg/proc/target.go:352

	}
	if g.Thread != nil {
		return t.SwitchThread(g.Thread.ThreadID())
	}
	t.selectedGoroutine = g
	return nil
}

// SwitchThread will change the selected and active thread.
func (t *Target) SwitchThread(tid int) error {
	if ok, err := t.Valid(); !ok {
		return err
	}
	if th, ok := t.FindThread(tid); ok {
		t.currentThread = th
		t.selectedGoroutine, _ = GetG(t.CurrentThread())
		return nil
	}
	return fmt.Errorf("thread %d does not exist", tid)
}

// setAsyncPreemptOff enables or disables async goroutine preemption by
// writing the value 'v' to runtime.debug.asyncpreemptoff.
// A value of '1' means off, a value of '0' means on.
func setAsyncPreemptOff(p *Target, v int64) {
	if producer := p.BinInfo().Producer(); producer == "" || !goversion.ProducerAfterOrEqual(producer, 1, 14) {
		return
	}
	logger := p.BinInfo().logger
	scope := globalScope(p, p.BinInfo(), p.BinInfo().Images[0], p.Memory())
	// +rtype -var debug anytype
	debugv, err := scope.findGlobal("runtime", "debug")
	if err != nil {
		logger.Warnf("could not find runtime/debug variable (or unreadable): %v", err)
		return
	}
	if debugv.Unreadable != nil {

View on GitHub (pinned to a23773e6c3)

Solutions

  1. Re-list threads with `threads` (or rpc2 ListThreads) and switch to a currently existing tid
  2. Re-select the goroutine instead of a raw thread (`goroutine <id>`) so Delve resolves a valid current thread
  3. Refresh debugger state after continue/step — old thread IDs are invalidated as threads exit
  4. If using gdbserial/core backends, ensure the thread belongs to the currently focused target in a multi-process (follow-exec) session

Example fix

// before
// stale id from an earlier list
err := tgt.SwitchThread(12345) // thread exited since
// after
threads, _ := tgt.ThreadList()
for _, th := range threads {
    if th.ThreadID() == tid {
        err = tgt.SwitchThread(tid)
        break
    }
}
Defensive patterns

Strategy: validation

Validate before calling

if _, ok := tgt.FindThread(tid); !ok {
    // refresh thread list before switching
    threads, _ := tgt.ThreadList()
    // pick a valid tid or abort
    return
}
err := tgt.SwitchThread(tid)

Type guard

func threadExists(t *Target, tid int) bool {
    _, ok := t.FindThread(tid)
    return ok
}

Try / catch

if err := dbg.SwitchGoroutine(false, gid); err != nil {
    if strings.Contains(err.Error(), "does not exist") {
        // refresh state and retry with a current id
        state, _ := dbg.State(false)
        _ = dbg.SwitchGoroutine(false, state.SelectedGoroutine.ID)
    }
}

Prevention

When it happens

Trigger: Calling SwitchThread (directly or via SwitchGoroutine, setCurrentThreads, pickCurrentThread, or RPC2/terminal 'thread <id>' / 'goroutine <id>') with a tid that is not in the target's current thread list — typically because the OS thread exited after the caller obtained the ID.

Common situations: Stale thread IDs captured before the debuggee exited threads (common with Go's M exiting); scripting the terminal `threads`/`thread` commands with outdated IDs; attaching/detaching races; picking a thread of a child process that has been reaped after an exec/fork follow.

Related errors


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