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
- Treat NotFound from the resume path as 'already exited' — log and continue teardown instead of failing the shutdown
- If it fires on every run, check why the target dies immediately (run the command outside turbo, look at its exit status)
- 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
- Treat thread-snapshot races at teardown as expected
- Don't gate shutdown success on resuming processes that may have died
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
- error_removing_node_modules
- error_removing_node_modules
- anchored system path is relative: {}
- Windows returned invalid UTF-16 path: {e}
- socket path has no parent: {sock_path}
AI-assisted analysis of vercel/turborepo@f9245100cf (2026-08-17).
Data as JSON: /api/errors/ed140a8cdced243c.
Report an issue: GitHub.