sinelaw/fresh · error
Plugin thread shut down
Error message
Plugin thread shut down
What it means
load_plugin() sends a LoadPlugin request over a oneshot channel to a dedicated plugin worker thread. This error is raised when the runtime's `request_sender` is `None`, meaning the plugin thread has already been shut down (e.g. `shutdown()` was called or the handle was created after teardown) and no receiver exists to accept the request. The library refuses to dispatch work to a dead worker rather than hang or silently drop the load.
Solutions
- Ensure the plugin thread is started (and not yet shut down) before calling load_plugin; check the runtime's lifecycle ordering.
- Guard call sites so plugin loading is skipped once shutdown has begun (e.g. an AtomicBool 'shutting_down' checked before load_plugin).
- If the thread exited unexpectedly, recreate the runtime handle by spawning a new plugin thread instead of reusing the old handle.
- Match on this error and surface a clear 'plugin runtime unavailable' message instead of retrying.
Example fix
// before
runtime.shutdown();
runtime.load_plugin(&plugin_path)?;
// after
if !runtime.is_shutdown() {
runtime.load_plugin(&plugin_path)?;
} else {
eprintln!("plugin runtime already shut down; skipping load of {}", plugin_path.display());
} Defensive patterns
Strategy: try-catch
Validate before calling
// Rust
fn can_load(rt: &PluginRuntimeHandle, shutting_down: &AtomicBool) -> bool {
!shutting_down.load(Ordering::SeqCst) && rt.is_running()
} Type guard
fn is_live(sender: &Option<Sender<PluginRequest>>) -> bool { sender.is_some() } Try / catch
match rt.load_plugin(&path) {
Ok(()) => { /* plugin loaded */ }
Err(e) if e.to_string().contains("Plugin thread shut down") => {
warn!("plugin runtime stopped; not loading {}", path.display());
}
Err(e) => return Err(e),
} Prevention
- Establish a strict lifecycle: start thread -> load plugins -> ... -> unload -> shutdown.
- Expose and check an is_running()/is_shutdown() flag before every plugin API call.
- Never reuse a runtime handle after shutdown; create a new one if needed.
- Add a shutdown barrier so background tasks finish before teardown.
When it happens
Trigger: Calling `PluginRuntimeHandle::load_plugin(path)` after the plugin thread has been stopped (`request_sender` set to `None`), or on a handle whose worker thread already exited/panicked and was joined.
Common situations: Shutting the editor/plugin runtime down while a background task still tries to load a plugin; calling load_plugin from a drop path or destructor that runs after shutdown; reusing a stale handle across application restart phases.
Related errors
AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13).
Data as JSON: /api/errors/f0ca5afb8b2262a2.
Report an issue: GitHub.
Appendix: source
Thrown at crates/fresh-plugin-runtime/src/thread.rs:574
.lock()
.ok()
.and_then(|mut owners| owners.remove(&request_id));
if let Some(plugin_name) = plugin_name {
if let Some(sender) = self.request_sender.as_ref() {
fire_and_forget(sender.send(PluginRequest::TrackAsyncResource {
plugin_name,
resource,
}));
}
}
}
/// Load a plugin from a file (blocking)
pub fn load_plugin(&self, path: &Path) -> Result<()> {
let (tx, rx) = oneshot::channel();
self.request_sender
.as_ref()
.ok_or_else(|| anyhow!("Plugin thread shut down"))?
.send(PluginRequest::LoadPlugin {
path: path.to_path_buf(),
response: tx,
})
.map_err(|_| anyhow!("Plugin thread not responding"))?;
rx.recv().map_err(|_| anyhow!("Plugin thread closed"))?
}
/// Load all plugins from a directory (blocking)
pub fn load_plugins_from_dir(&self, dir: &Path) -> Vec<String> {
let (tx, rx) = oneshot::channel();
let Some(sender) = self.request_sender.as_ref() else {
return vec!["Plugin thread shut down".to_string()];
};
if sender
.send(PluginRequest::LoadPluginsFromDir {
dir: dir.to_path_buf(),View on GitHub (pinned to 67894ca546)