emilk/egui · error

failed to spawn thread

Error message

failed to spawn thread

What it means

egui_extras' file loader spawns a background OS thread to perform blocking file reads so load() can return BytesPoll::Pending immediately. It calls thread::Builder::spawn(...).expect("failed to spawn thread"), which panics when the OS refuses to create a thread — usually because the process hit the thread or memory limit. Any egui app loading files via Image::from_uri on the file loader hits this path.

Solutions

  1. Raise the process/thread limit (ulimit -u, Docker --pids-limit, cgroup pids.max) or free memory
  2. Check the app for leaked/stuck loader threads — count them (e.g. /proc/<pid>/status Threads) and fix URIs that never resolve
  3. Pin egui_extras to a version whose file loader reuses a thread pool or handles spawn failure gracefully, or implement a custom ImageLoader that returns Ok(BytesPoll::Pending) when spawn fails
  4. Retry the load later once system pressure subsides
Defensive patterns

Strategy: retry

Validate before calling

// Before heavy image loading, check headroom for another thread:
let threads = std::fs::read_to_string("/proc/self/status").ok()
    .and_then(|s| s.lines().find(|l| l.starts_with("Threads:"))?.split_whitespace().last()?.parse::<u32>().ok());
if let Some(t) = threads { eprintln!("thread count: {t}"); }

Try / catch

// Wrap loading so a spawn failure degrades instead of panicking:
fn poll_uri(ctx: &Context, uri: &str) -> BytesPoll {
    match std::panic::catch_unwind(|| loader.load(ctx, uri)) {
        Ok(poll) => poll,
        Err(_) => BytesPoll::Pending { size: None }, // retry later
    }
}

Prevention

When it happens

Trigger: Calling load() on the FileLoader when std::thread::Builder::spawn returns Err — the process has exhausted the OS thread limit (RLIMIT_NPROC / cgroup pids.max / threads-max), is out of virtual memory for a new stack, or is running in a restricted sandbox that forbids thread creation.

Common situations: CI or Docker containers with low pids limits; apps that leak loader threads (one per Image that never finishes loading, e.g. missing files or a spinning disk stall) until the limit is hit; embedded/minimal environments with strict resource cgroups; heavily loaded hosts at the kernel threads-max cap.

Related errors


AI-assisted analysis of emilk/egui@441971a776 (2026-09-12). Data as JSON: /api/errors/ec6a8d1ca2d597bd. Report an issue: GitHub.

Appendix: source

Thrown at crates/egui_extras/src/loaders/file_loader.rs:136

                            let mut cache = cache.lock();
                            if let std::collections::hash_map::Entry::Occupied(mut entry) = cache.entry(uri.clone()) {
                                let entry = entry.get_mut();
                                *entry = Poll::Ready(result);
                                log::trace!("Finished loading {uri:?}");
                                true
                            } else {
                                log::trace!("Canceled loading {uri:?}\nNote: This can happen if `forget_image` is called while the image is still loading.");
                                false
                            }
                        };
                        // We may not lock Context while the cache lock is held (see ImageLoader::load
                        // for details).
                        if repaint {
                            ctx.request_repaint();
                        }
                    }
                })
                .expect("failed to spawn thread");

            Ok(BytesPoll::Pending { size: None })
        }
    }

    fn forget(&self, uri: &str) {
        let _ = self.cache.lock().remove(uri);
    }

    fn forget_all(&self) {
        self.cache.lock().clear();
    }

    fn byte_size(&self) -> usize {
        self.cache
            .lock()
            .values()
            .map(|entry| match entry {

View on GitHub (pinned to 441971a776)