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
- Audit recent changes to handle_task_completion and task spawning for paths that overwrite task.coroutine_id
- Check that task cancellation never dec_refs the coroutine while task.coroutine_id still points at it
- Add a debug assertion when setting task.coroutine_id that the heap entry is a Coroutine
- File a bug with a reproducing script if hit from normal async code
Defensive patterns
Strategy: fallback
Prevention
- Treat any such panic as an interpreter bug: capture the script and report it
- Run async test suites with --features memory-model-checks when touching refcount-sensitive code
- Avoid modifying task.coroutine_id outside load_or_init_task/init paths
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
- task has no frames and no coroutine_id
- current task is in unexpected Completed state after resolvin
- pending_externals entry doesn't point to an ExternalFuture
- Awaiter::GatherSlot gather id is not a GatherFuture
- resolve_child called on a gather that was never awaited
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/ee3b56ea3116f46b.
Report an issue: GitHub.