embassy-rs/embassy · error
CallFuture polled after completion
Error message
CallFuture polled after completion
What it means
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.
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.
Example fix
// before
if matches!(fut.poll(cx), Poll::Ready(r)) { done = true; }
fut.poll(cx); // polled again after done
// after
match fut.poll(cx) {
Poll::Ready(r) => return Poll::Ready(r),
Poll::Pending => return Poll::Pending,
} Defensive patterns
Strategy: validation
Validate before calling
// Stop polling once Ready is observed:
enum State { Running(CallFuture<..>), Done }
fn poll_once(state: &mut State, cx: &mut Context) -> Poll<R> {
match state {
State::Running(f) => match f.poll_unpin(cx) {
Poll::Ready(r) => { *state = State::Done; Poll::Ready(r) }
Poll::Pending => Poll::Pending,
},
State::Done => panic!("do not re-poll"),
}
} Type guard
fn is_done<T>(r: &Poll<T>) -> bool { matches!(r, Poll::Ready(_)) } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Bad mode
- RpcService::run() must not be called concurrently
- Passphrase is too short or too long
- Boot prepare error
- Boot prepare error
AI-assisted analysis of embassy-rs/embassy@463a07b963 (2026-09-10).
Data as JSON: /api/errors/e6d7a471df44f178.
Report an issue: GitHub.
Appendix: source
Thrown at embassy-sync/src/rpc_service.rs:620
Phase::Acquiring => match self.svc.slot.poll_acquire(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(()) => {
let f = self.f.take().unwrap();
// SAFETY: we just acquired the slot, F and R fit (compile-time check in call())
unsafe { self.svc.slot.submit::<R, F>(f) };
self.phase = Phase::Submitted;
}
},
Phase::Submitted => {
return match self.svc.slot.poll_result::<R>(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(result) => {
self.phase = Phase::Done;
Poll::Ready(result)
}
};
}
Phase::Done => panic!("CallFuture polled after completion"),
}
}
}
}
impl<M: RawMutex, T, R, F, const S: usize> Drop for CallFuture<'_, M, T, R, F, S> {
fn drop(&mut self) {
if matches!(self.phase, Phase::Submitted) {
// Future dropped after the closure was written to the slot.
// The runner will still finish executing the closure when polled, but we cannot
// touch the slot (because the runner may still be using it). We also must not
// block, so we signal ack so the runner can cleanup and accept new work.
// The two ack paths are mutually exclusive. Either the poll completes,
// so ack is sent in poll_result and phase moves to Done synchronously – or we're
// dropped while still in Submitted and the ack is sent here. Never both.
self.svc.slot.ack.signal(());
}
}View on GitHub (pinned to 463a07b963)