{"record":{"id":"1fee63609f3d1cb7","repo":"slint-ui/slint","slug":"polling-completed-or-aborted-joinhandle","errorCode":null,"errorMessage":"Polling completed or aborted JoinHandle","messagePattern":"Polling completed or aborted JoinHandle","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/core/future.rs","lineNumber":110,"sourceCode":"///\n/// This trait implements future. Polling it after it finished or aborted may result in a panic.\npub struct JoinHandle<T>(alloc::sync::Arc<FutureRunner<T>>);\n\nimpl<T> Future for JoinHandle<T> {\n    type Output = T;\n\n    fn poll(self: Pin<&mut Self>, cx: &mut core::task::Context<'_>) -> Poll<Self::Output> {\n        let mut inner = self.0.inner();\n        match &mut inner.fut {\n            FutureState::Running(_) => {\n                let waker = cx.waker();\n                if !inner.wakers.iter().any(|w| w.will_wake(waker)) {\n                    inner.wakers.push(waker.clone());\n                }\n                Poll::Pending\n            }\n            FutureState::Finished(x) => {\n                Poll::Ready(x.take().expect(\"Polling completed or aborted JoinHandle\"))\n            }\n        }\n    }\n}\n\nimpl<T> JoinHandle<T> {\n    /// If the future hasn't completed yet, this will make the event loop stop polling the corresponding future and it will be dropped\n    ///\n    /// Once this handle has been aborted, it can no longer be polled\n    pub fn abort(self) {\n        self.0.aborted.store(true, atomic::Ordering::Relaxed);\n    }\n    /// Checks if the task associated with this `JoinHandle` has finished.\n    pub fn is_finished(&self) -> bool {\n        matches!(self.0.inner().fut, FutureState::Finished(_))\n    }\n}\n","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/slint-ui/slint/blob/3fd8f2ec03c2aa8a95d5f4b9daa299c7b8bf4016/internal/core/future.rs#L92-L128","documentation":"JoinHandle<T> is the future returned by slint's spawn_local() (SlintContext::spawn_local). When the spawned task finishes, the inner state becomes FutureState::Finished(Some(val)) and the first poll consumes the value with Option::take(); a second poll, or any poll after abort() (which finishes with None), finds an empty slot and panics via .expect. The struct's own doc warns: 'Polling it after it finished or aborted may result in a panic.'","triggerScenarios":"Awaiting or manually polling the same JoinHandle twice (e.g. a combinator or hand-written Future that keeps polling after Ready); awaiting a handle after calling handle.abort(); storing the handle in a wrapper future that an executor re-polls after completion.","commonSituations":"Wrapping slint::spawn_local handles in futures::select!/timeout combinators that may re-poll; racing abort() against an .await; sharing the handle across code paths where each tries to await it; porting code from tokio::task::JoinHandle which permits extra polls.","solutions":["Await each JoinHandle exactly once, then drop it; treat the first Ready as final.","Never await a handle after calling abort() - abort() is terminal, just drop the handle.","If the result is needed in several places, share the value (Rc<OnceCell>, a channel, or futures::future::Shared on a compatible handle), not the handle itself.","When writing a manual poll fn around the handle, follow the Future contract: never emit Ready twice and stop polling after Ready.","Use handle.is_finished() only to skip/inspect state, never as a license to poll again."],"exampleFix":"// before: the same handle is awaited twice\nlet handle = slint::spawn_local(async { 42 })?;\nlet a = handle.await;\nlet b = handle.await; // PANIC: 'Polling completed or aborted JoinHandle'\n\n// after: await once, share the value instead of the handle\nlet handle = slint::spawn_local(async { 42 })?;\nlet value = handle.await; // first and only consuming poll\nlet b = value;            // reuse `value` everywhere else","handlingStrategy":"validation","validationCode":"// Treat a finished handle as spent: never await/poll it again.\n// is_finished() == true means the value was already consumed\n// (or the task was aborted and Finished(None)).\nif !handle.is_finished() {\n    let value = handle.await; // first and only consuming poll\n    // use `value` from here on\n} else {\n    // handle already awaited or aborted: read the stored result instead\n}","typeGuard":null,"tryCatchPattern":"// Last resort if a third-party combinator may re-poll the handle:\nlet result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n    futures::executor::block_on(handle)\n}));\n// Fix the double-poll at the call site; this only contains the blast radius.","preventionTips":["Await each slint::spawn_local JoinHandle exactly once, then drop it","Treat abort() as terminal - drop the handle, never await it afterwards","Do not store JoinHandle in wrappers that may be polled after Ready; share the result (OnceCell, channel) instead","In hand-written poll functions, honor the Future contract: Ready is emitted at most once"],"tags":["rust","async","join-handle","panic","spawn-local"],"backgroundTag":"future-polled-after-completion","analyzedSha":"3fd8f2ec03c2aa8a95d5f4b9daa299c7b8bf4016","analyzedAt":"2026-08-19T23:23:16.610Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}