actix/actix · error · panic

MapErr must not be polled after it returned `Poll::Ready`

Error message

MapErr must not be polled after it returned `Poll::Ready`

What it means

`MapErr` transforms the error of a `TryActorFuture` via a closure. After it yields `Poll::Ready` it moves to `Complete`, having consumed the closure `f`; any further poll cannot proceed, so actix panics with `MapErr must not be polled after it returned Poll::Ready`.

Solutions

  1. Treat `Poll::Ready` as terminal: drop the `MapErr` future immediately after.
  2. In manual polling loops, remove futures that returned `Ready`.
  3. Delegate polling to actix's spawn/context machinery instead of calling `try_poll` yourself.
  4. If you need the mapped error again, store the result rather than re-polling.

Example fix

// before
match fut.poll(act, ctx, task) {
    Poll::Ready(res) => results.push(res),
    Poll::Pending => {}
}
// next tick: fut still in vec, polled again -> panic

// after
poll_set.retain_mut(|fut| fut.poll(act, ctx, task).is_pending());
Defensive patterns

Strategy: try-catch

Validate before calling

if mapped_done { return Poll::Ready(cached_result); }

Try / catch

#[should_panic(expected = "MapErr must not be polled")] // tests only

Prevention

When it happens

Trigger: Polling a `MapErr`-wrapped try future after completion — retaining it in a poll-everything loop, or re-polling within nested combinators after the inner future resolved.

Common situations: Error-handling layers stacked on actor futures that are stored and re-polled; manual future drivers in actors; tests that poll a resolved future to check idempotency.

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


AI-assisted analysis of actix/actix@36e5d97e41 (2026-09-11). Data as JSON: /api/errors/c43e839e93a56ff3. Report an issue: GitHub.

Appendix: source

Thrown at actix/src/fut/try_future/map_err.rs:61

    fn poll(
        mut self: Pin<&mut Self>,
        act: &mut A,
        ctx: &mut A::Context,
        task: &mut Context<'_>,
    ) -> Poll<Self::Output> {
        match self.as_mut().project() {
            MapProj::Incomplete { future, .. } => {
                let output = ready!(future.try_poll(act, ctx, task));
                match self.project_replace(MapErr::Complete) {
                    MapProjReplace::Incomplete { f, .. } => {
                        Poll::Ready(output.map_err(|err| f(err, act, ctx)))
                    }
                    MapProjReplace::Complete => unreachable!(),
                }
            }
            MapProj::Complete => {
                panic!("MapErr must not be polled after it returned `Poll::Ready`")
            }
        }
    }
}

View on GitHub (pinned to 36e5d97e41)