{"record":{"id":"20ac484a10755f54","repo":"jdx/mise","slug":"plugin-task-name-should-be-registered-before-spawn","errorCode":null,"errorMessage":"plugin task name should be registered before spawning","messagePattern":"plugin task name should be registered before spawning","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/cli/plugins/mod.rs","lineNumber":23,"sourceCode":"use eyre::{Report, Result, WrapErr, eyre};\nuse tokio::task::{Id, JoinSet};\n\nuse crate::config::Config;\n\npub(crate) mod install;\nmod link;\nmod ls;\nmod ls_remote;\nmod uninstall;\nmod update;\n\ntype PluginTaskResult = Result<()>;\ntype PluginTaskNames = HashMap<Id, String>;\n\nfn take_plugin_name(task_names: &mut PluginTaskNames, id: Id) -> String {\n    task_names\n        .remove(&id)\n        .expect(\"plugin task name should be registered before spawning\")\n}\n\nfn spawn_plugin_task<F>(\n    tasks: &mut JoinSet<PluginTaskResult>,\n    task_names: &mut PluginTaskNames,\n    plugin: impl Into<String>,\n    task: F,\n) where\n    F: Future<Output = PluginTaskResult> + Send + 'static,\n{\n    let task = tasks.spawn(task);\n    task_names.insert(task.id(), plugin.into());\n}\n\nasync fn join_plugin_tasks(\n    mut tasks: JoinSet<PluginTaskResult>,\n    mut task_names: PluginTaskNames,\n    operation: &'static str,","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/cli/plugins/mod.rs#L5-L41","documentation":"A `.expect()` panic in `take_plugin_name` (src/cli/plugins/mod.rs:23). Every plugin task spawned via `spawn_plugin_task` must register a display name in `PluginTaskNames` keyed by the JoinSet task Id; `join_plugin_tasks` removes that name when the task finishes. Panicking means a completed task's Id was never registered — spawn bypassed the registration helper.","triggerScenarios":"A plugin task spawned directly on the JoinSet (not via `spawn_plugin_task`), or registration failing/being skipped before `detach`, so `join_plugin_tasks` receives an unknown Id.","commonSituations":"New code paths spawning plugin tasks after a refactor; a spawn helper changed so names are inserted after the task already completed and were drained.","solutions":["Spawn all plugin tasks through `spawn_plugin_task` so the name is registered before the task runs","Ensure the name insert happens before `JoinSet::spawn` returns its Id","Make take_plugin_name fall back to a placeholder (e.g. \"<unknown plugin>\") instead of panicking"],"exampleFix":"// before\ntask_names.remove(&id).expect(\"plugin task name should be registered before spawning\")\n// after\ntask_names.remove(&id).unwrap_or_else(|| \"<unknown plugin>\".to_string())","handlingStrategy":"type-guard","validationCode":"debug_assert!(task_names.contains_key(&id), \"plugin task name not registered for {id:?}\");","typeGuard":"fn registered(task_names: &PluginTaskNames, id: &Id) -> bool { task_names.contains_key(id) }","tryCatchPattern":"let name = task_names.remove(&id).unwrap_or_else(|| \"<unregistered plugin>\".into());","preventionTips":["Route every spawn through spawn_plugin_task; never call JoinSet::spawn directly for plugin tasks","Insert the task name before spawning, not after","Add a unit test that joins at least one spawned plugin task"],"tags":["rust","panic","plugins","concurrency"],"backgroundTag":"internal-invariant-violation","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}