PyO3/pyo3 · error

unexpected error in coroutine waker

Error message

unexpected error in coroutine waker

What it means

This panic fires inside a Rust `#[pyclass]` coroutine's `wake_by_ref` when the stored loop-and-future slot reports an error while setting the wake result, i.e. the waker's internal `Once`-style state was initialized to a value that cannot produce a `set_result`. It signals a corrupted or never-properly-initialized coroutine waker rather than a user-facing Python error. The `get_or_init(..., || None)` path stored `None` on first use, so this branch should only run when a loop/future pair actually exists.

Source

Thrown at src/coroutine/waker.rs:48

        py: Python<'py>,
    ) -> PyResult<Option<&Bound<'py, PyAny>>> {
        let init = || LoopAndFuture::new(py).map(Some);
        let loop_and_future = self.0.get_or_try_init(py, init)?.as_ref();
        Ok(loop_and_future.map(|LoopAndFuture { future, .. }| future.bind(py)))
    }
}

impl Wake for AsyncioWaker {
    fn wake(self: Arc<Self>) {
        self.wake_by_ref()
    }

    fn wake_by_ref(self: &Arc<Self>) {
        Python::attach(|py| {
            if let Some(loop_and_future) = self.0.get_or_init(py, || None) {
                loop_and_future
                    .set_result(py)
                    .expect("unexpected error in coroutine waker");
            }
        });
    }
}

struct LoopAndFuture {
    event_loop: Py<PyAny>,
    future: Py<PyAny>,
}

impl LoopAndFuture {
    fn new(py: Python<'_>) -> PyResult<Self> {
        static GET_RUNNING_LOOP: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
        let import = || -> PyResult<_> {
            let module = py.import("asyncio")?;
            Ok(module.getattr("get_running_loop")?.into())
        };
        let event_loop = GET_RUNNING_LOOP.get_or_try_init(py, import)?.call0(py)?;

View on GitHub (pinned to ac9b6899d3)

Solutions

  1. Ensure the coroutine future is not dropped or resolved before its waker is invoked (do not cancel the asyncio task while the Rust side still holds the waker)
  2. Keep the event loop alive for the duration of the coroutine; do not close the loop before awaiting finishes
  3. Upgrade pyo3 — waker state management has been reworked in newer releases
  4. Reproduce with a minimal example and report to PyO3 if the panic occurs in normal awaiting

Example fix

// before
let task = asyncio.create_task(coro); task.cancel(); await asyncio.sleep(0)
// after
await task  # let the coroutine finish before dropping/awaking its waker
Defensive patterns

Strategy: try-catch

Validate before calling

# Python: ensure the asyncio task/loop is alive before interacting
assert not task.done(), 'coroutine task already finished; do not wake its waker'

Type guard

def is_task_alive(task) -> bool:
    return task is not None and not task.done() and not task.cancelled()

Try / catch

try:
    result = await rust_coroutine(...)
except asyncio.CancelledError:
    # do not touch the waker afterwards
    pass

Prevention

When it happens

Trigger: Waking an already-completed or dropped coroutine task; the future slot was consumed/cleared between scheduling and wake; calling `wake_by_ref` after the event loop future was already resolved.

Common situations: Long-running async Rust functions wrapped with pyo3's coroutine support; cancelling Python `asyncio` tasks whose Rust waker still fires; mixed sync/async shutdown ordering at interpreter teardown.

Related errors


AI-assisted analysis of PyO3/pyo3@ac9b6899d3 (2026-09-05). Data as JSON: /api/errors/93467f2e53ab43a4. Report an issue: GitHub.