pydantic/monty · critical

task coroutine_id doesn't point to a Coroutine

Error message

task coroutine_id doesn't point to a Coroutine

What it means

This is an internal consistency panic in Monty's async scheduler: when a spawned task completes, handle_task_completion reads the task's coroutine from the heap and expects the stored HeapId to resolve to a Coroutine entry. If it resolves to any other heap type, the scheduler's internal bookkeeping is corrupted. It is not a Python-level error and cannot be triggered by valid user code.

Source

Thrown at crates/monty/src/bytecode/vm/async_exec.rs:455

    pub(super) fn handle_task_completion(&mut self, result: Value) -> Result<AwaitResult, RunError> {
        let task_id = self
            .scheduler
            .current_task_id()
            .expect("handle_task_completion called without current task");
        // Take the awaiter before cancelling the task: it owns the inc_ref on
        // the gather it points at, so holding it here keeps that gather alive
        // across the teardown below.
        let task = self.scheduler.get_task_mut(task_id);
        let awaiter = task.awaiter.take();
        let coroutine_id = task
            .coroutine_id
            .expect("handle_task_completion: spawned task without a coroutine");

        // Mark the coroutine as Completed before the task is cancelled —
        // direct `await` of this coroutine elsewhere needs to see the new
        // state, not the `Running` it had until now.
        let HeapReadOutput::Coroutine(mut coro) = self.heap.read(coroutine_id) else {
            panic!("task coroutine_id doesn't point to a Coroutine")
        };
        coro.get_mut(self.heap).state = CoroutineState::Completed;
        drop(coro);

        // Cancel the task now to release its inc_ref on the coroutine;
        // otherwise it would linger in the scheduler. Its awaiter is already
        // out, so this releases nothing the delivery below needs.
        self.scheduler.cancel_task(task_id, self.heap);

        // Hand the result down the chain. `None` means the gather that spawned
        // this task settled first, so it ran on only for its side effects.
        let delivery = if let Some(awaiter) = awaiter {
            self.deliver_awaiter_success(awaiter, result)
        } else {
            result.drop_with(self);
            None
        };

View on GitHub (pinned to adc986b362)

Solutions

  1. Audit recent changes to handle_task_completion and task spawning for paths that overwrite task.coroutine_id
  2. Check that task cancellation never dec_refs the coroutine while task.coroutine_id still points at it
  3. Add a debug assertion when setting task.coroutine_id that the heap entry is a Coroutine
  4. File a bug with a reproducing script if hit from normal async code
Defensive patterns

Strategy: fallback

Prevention

When it happens

Trigger: Only reachable if the task's coroutine_id field was mutated or the heap entry was freed and its slot reused for a non-Coroutine object, i.e. a reference-counting or GC bug in the interpreter itself.

Common situations: Developers hit this while hacking on monty's async runtime (async_exec.rs), after modifying task spawning/cancellation paths, or after changes to heap entry reuse without updating task state.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13). Data as JSON: /api/errors/ee3b56ea3116f46b. Report an issue: GitHub.