vercel/turborepo · warning · std::io::Error

no threads found for process {process_id}

Error message

no threads found for process {process_id}

What it means

On Windows, resume_process (job_object.rs:440 region) walks a Thread32First/Thread32Next snapshot and resumes (OpenThread + ResumeThread) every thread whose th32OwnerProcessID matches. If the walk completes with no thread owned by that process, it returns NotFound 'no threads found for process {id}'. The process has no threads left, i.e. it already exited between suspension and resume.

Source

Thrown at crates/turborepo-process/src/job_object.rs:440

        tpDeltaPri: 0,
        dwFlags: 0,
    };

    let mut found_thread = false;
    let mut has_entry = unsafe { Thread32First(snapshot, &mut entry) } != 0;
    while has_entry {
        if entry.th32OwnerProcessID == process_id {
            found_thread = true;
            resume_thread(entry.th32ThreadID)?;
        }

        has_entry = unsafe { Thread32Next(snapshot, &mut entry) } != 0;
    }

    if found_thread {
        Ok(())
    } else {
        Err(io::Error::new(
            io::ErrorKind::NotFound,
            format!("no threads found for process {process_id}"),
        ))
    }
}

fn resume_thread(thread_id: u32) -> io::Result<()> {
    let thread_handle = unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, thread_id) };
    if thread_handle.is_null() {
        return Err(io::Error::last_os_error());
    }

    let resume_result = unsafe { ResumeThread(thread_handle) };
    let resume_error = (resume_result == u32::MAX).then(io::Error::last_os_error);
    let close_result = unsafe { CloseHandle(thread_handle) };

    if let Some(err) = resume_error {
        return Err(err);

View on GitHub (pinned to f9245100cf)

Solutions

  1. Treat NotFound from the resume path as 'already exited' — log and continue teardown instead of failing the shutdown
  2. If it fires on every run, check why the target dies immediately (run the command outside turbo, look at its exit status)
  3. Avoid suspending/resuming processes that you also allow to self-exit quickly

Example fix

// before
resume_process(pid)?;
// after — already-exited is success during teardown
if let Err(e) = resume_process(pid) {
    if e.kind() != std::io::ErrorKind::NotFound {
        return Err(e);
    }
    tracing::debug!(pid, "process exited before resume");
}
Defensive patterns

Strategy: fallback

Validate before calling

// before resuming, check the process still exists
if !proc_exists(pid) { return Ok(()); } // already gone
fn proc_exists(pid: u32) -> bool {
    std::path::Path::new(&format!("/proc/{pid}")).exists() // unix; use OpenProcess on windows
}

Try / catch

// NotFound during resume == process exited: swallow and continue teardown
if let Err(e) = resume_process(pid) {
    if e.kind() != std::io::ErrorKind::NotFound { return Err(e); }
    tracing::debug!(pid, "already exited before resume");
}

Prevention

When it happens

Trigger: Resuming a process in a job object after it exited while suspended or during teardown — e.g. the Ctrl+C/shutdown path resuming children that have already died, or a child that crashed immediately after start.

Common situations: Very short-lived child processes exiting before resume Force-killed trees during cancellation Benign races at shutdown; the process is gone either way

Related errors


AI-assisted analysis of vercel/turborepo@f9245100cf (2026-08-17). Data as JSON: /api/errors/ed140a8cdced243c. Report an issue: GitHub.