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
- Raise the process/thread limit (ulimit -u, Docker --pids-limit, cgroup pids.max) or free memory
- Check the app for leaked/stuck loader threads — count them (e.g. /proc/<pid>/status Threads) and fix URIs that never resolve
- 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
- 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
- Set generous --pids-limit / ulimit -u in containers running egui apps
- Watch for loader thread leaks: every Image::from_uri that never resolves keeps a thread alive
- Prefer a custom/pooled loader for apps that load many files concurrently
- Retry loads with backoff under memory or thread pressure instead of spawning more
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
- A style should be set for
- app_mut got the wrong type of App
- Context::root_ui was called while the root Ui was already…
- Could not create Date
- Could not create Date
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)