{"record":{"id":"e6d7a471df44f178","repo":"embassy-rs/embassy","slug":"callfuture-polled-after-completion","errorCode":null,"errorMessage":"CallFuture polled after completion","messagePattern":"CallFuture polled after completion","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"embassy-sync/src/rpc_service.rs","lineNumber":620,"sourceCode":"                Phase::Acquiring => match self.svc.slot.poll_acquire(cx) {\n                    Poll::Pending => return Poll::Pending,\n                    Poll::Ready(()) => {\n                        let f = self.f.take().unwrap();\n                        // SAFETY: we just acquired the slot, F and R fit (compile-time check in call())\n                        unsafe { self.svc.slot.submit::<R, F>(f) };\n                        self.phase = Phase::Submitted;\n                    }\n                },\n                Phase::Submitted => {\n                    return match self.svc.slot.poll_result::<R>(cx) {\n                        Poll::Pending => Poll::Pending,\n                        Poll::Ready(result) => {\n                            self.phase = Phase::Done;\n                            Poll::Ready(result)\n                        }\n                    };\n                }\n                Phase::Done => panic!(\"CallFuture polled after completion\"),\n            }\n        }\n    }\n}\n\nimpl<M: RawMutex, T, R, F, const S: usize> Drop for CallFuture<'_, M, T, R, F, S> {\n    fn drop(&mut self) {\n        if matches!(self.phase, Phase::Submitted) {\n            // Future dropped after the closure was written to the slot.\n            // The runner will still finish executing the closure when polled, but we cannot\n            // touch the slot (because the runner may still be using it). We also must not\n            // block, so we signal ack so the runner can cleanup and accept new work.\n            // The two ack paths are mutually exclusive. Either the poll completes,\n            // so ack is sent in poll_result and phase moves to Done synchronously – or we're\n            // dropped while still in Submitted and the ack is sent here. Never both.\n            self.svc.slot.ack.signal(());\n        }\n    }","sourceCodeStart":602,"sourceCodeEnd":638,"githubUrl":"https://github.com/embassy-rs/embassy/blob/463a07b963419a1bfe61d5d597c44acb810afb8b/embassy-sync/src/rpc_service.rs#L602-L638","documentation":"CallFuture is an in-flight RPC call future with an explicit Phase state machine. Once it reaches Phase::Done it must never be polled again; polling a completed CallFuture panics. This is an internal-use invariant — futures must not be polled after returning Ready.","triggerScenarios":"Continuing to poll a CallFuture after it has returned Poll::Ready — e.g. manual poll() loops that ignore Ready, storing the future and polling it after completion, or a select that polls a stale future.","commonSituations":"Hand-rolled poll loops instead of .await; keeping the CallFuture in a registry after completion and re-polling; misuse of the future inside a custom executor.","solutions":["Await the CallFuture with .await and let Drop run at completion; never poll manually after Ready.","If manual polling is required, stop polling as soon as Poll::Ready is returned.","Fuse the future (wrap so polls after Ready return Pending/Ready(None)) or track completion yourself before re-polling."],"exampleFix":"// before\nif matches!(fut.poll(cx), Poll::Ready(r)) { done = true; }\nfut.poll(cx); // polled again after done\n// after\nmatch fut.poll(cx) {\n    Poll::Ready(r) => return Poll::Ready(r),\n    Poll::Pending => return Poll::Pending,\n}","handlingStrategy":"validation","validationCode":"// Stop polling once Ready is observed:\nenum State { Running(CallFuture<..>), Done }\nfn poll_once(state: &mut State, cx: &mut Context) -> Poll<R> {\n    match state {\n        State::Running(f) => match f.poll_unpin(cx) {\n            Poll::Ready(r) => { *state = State::Done; Poll::Ready(r) }\n            Poll::Pending => Poll::Pending,\n        },\n        State::Done => panic!(\"do not re-poll\"),\n    }\n}","typeGuard":"fn is_done<T>(r: &Poll<T>) -> bool { matches!(r, Poll::Ready(_)) }","tryCatchPattern":null,"preventionTips":["Use .await instead of manual poll loops","Drop or fuse futures after they complete","Never store CallFutures in registries past completion","In select-like code, remove completed futures from the poll set"],"tags":["embedded","rpc","async","future","panic"],"backgroundTag":"invalid-state-transition","analyzedSha":"463a07b963419a1bfe61d5d597c44acb810afb8b","analyzedAt":"2026-09-10T13:38:26.660Z","contentChangedAt":"2026-09-10T13:38:26.660Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}