{"record":{"id":"470aec6b2a6397e4","repo":"dbt-labs/dbt-core","slug":"joinhandle-polled-after-completion","errorCode":null,"errorMessage":"JoinHandle polled after completion","messagePattern":"JoinHandle polled after completion","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/dbt-runtime/src/task/core.rs","lineNumber":375,"sourceCode":"        // Safety: the caller ensures mutual exclusion to the field.\n        unsafe {\n            self.set_stage(Stage::Finished(output));\n        }\n    }\n\n    /// Takes the task output.\n    ///\n    /// # Safety\n    ///\n    /// The caller must ensure it is safe to mutate the `stage` field.\n    pub(super) fn take_output(&self) -> super::Result<T::Output> {\n        use std::mem;\n\n        self.stage.stage.with_mut(|ptr| {\n            // Safety:: the caller ensures mutual exclusion to the field.\n            match mem::replace(unsafe { &mut *ptr }, Stage::Consumed) {\n                Stage::Finished(output) => output,\n                _ => panic!(\"JoinHandle polled after completion\"),\n            }\n        })\n    }\n\n    unsafe fn set_stage(&self, stage: Stage<T>) {\n        let _guard = TaskIdGuard::enter(self.task_id);\n        self.stage.stage.with_mut(|ptr| *ptr = stage);\n    }\n}\n\nimpl Header {\n    /// Gets a pointer to the `Trailer` of the task containing this `Header`.\n    ///\n    /// # Safety\n    ///\n    /// The provided raw pointer must point at the header of a task.\n    pub(super) unsafe fn get_trailer(me: NonNull<Header>) -> NonNull<Trailer> {\n        let offset = me.as_ref().vtable.trailer_offset;","sourceCodeStart":357,"sourceCodeEnd":393,"githubUrl":"https://github.com/dbt-labs/dbt-core/blob/0267ce9170576975b76b64ce856b2e5848e96617/crates/dbt-runtime/src/task/core.rs#L357-L393","documentation":"A JoinHandle's output can be consumed only once. take_output replaces the internal stage with Stage::Consumed; if the stage is not Finished(output) at that moment (already consumed or still pending), the library panics that the JoinHandle was polled after completion/consumption. This guards the single-consumption contract of the task result.","triggerScenarios":"Polling or awaiting a JoinHandle after it already resolved and its output was taken — e.g. awaiting the same JoinHandle twice, polling it after an earlier poll returned Ready, or calling an internal API that takes the output more than once.","commonSituations":"Storing a JoinHandle in a collection and awaiting it from two places; re-polling after join() inside a retry loop; manually pinning and polling a JoinHandle in a custom executor and not dropping it after Ready.","solutions":["Await each JoinHandle exactly once; if you need shared results, use Arc on the output or a oneshot/watch channel to fan out","Check for double-await logic: remove the JoinHandle from your collection when polled to completion, or use a JoinSet","If polling manually, stop polling once Poll::Ready is returned and drop the handle"],"exampleFix":"// before\nlet out1 = (&mut jh).await;\nlet out2 = jh.await; // panic: polled after completion\n\n// after\nlet out = jh.await; // consume once; clone/Arc the result if needed elsewhere","handlingStrategy":"type-guard","validationCode":"// Track consumption yourself before awaiting twice\nstruct OnceJoin<T>(Option<tokio::task::JoinHandle<T>>);\nimpl<T> OnceJoin<T> { fn take(&mut self) -> tokio::task::JoinHandle<T> { self.0.take().expect(\"JoinHandle already consumed\") } }","typeGuard":"fn is_joinable<T>(jh: &Option<tokio::task::JoinHandle<T>>) -> bool { jh.is_some() }","tryCatchPattern":"let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| jh.clone()));\n// better: remove the handle from storage upon first await so re-await is a compile-time/logic error","preventionTips":["Await each JoinHandle exactly once; use JoinSet for many tasks","Remove handles from collections as soon as they are polled to Ready","Use Arc<Output> or channels for shared results instead of re-awaiting","Avoid manual polling of JoinHandles; rely on .await"],"tags":["rust","async","panic","double-consume"],"backgroundTag":"invalid-state-transition","analyzedSha":"0267ce9170576975b76b64ce856b2e5848e96617","analyzedAt":"2026-09-07T21:53:39.732Z","contentChangedAt":"2026-09-07T21:53:39.732Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}