pydantic/monty · critical
gather_id doesn't point to a GatherFuture
Error message
gather_id doesn't point to a GatherFuture
What it means
When a task awaiting a gather future fails, the scheduler looks up the gather future on the heap; this panic fires if the stored HeapId is not a `GatherFuture`. Gather ids come from the scheduler's own allocation, so this means heap corruption, premature free of the gather, or a scheduler bug — not a Python-level error.
Source
Thrown at crates/monty/src/bytecode/vm/scheduler.rs:513
None => None,
Some(Awaiter::Task(task_id)) => {
// A task's own awaiter is the `GatherSlot` its gather gave it;
// borrow that gather's id without taking the task's ref, which
// stays until the task is cancelled.
let gather_id = match self.tasks.get(&task_id).and_then(|t| t.awaiter.as_ref()) {
Some(Awaiter::GatherSlot { gather, .. }) => Some(*gather),
Some(Awaiter::Task(_)) | None => None,
};
match gather_id {
// The task that was awaiting the future is itself in a
// gather. Settle that gather here so the failure anchors
// at the same site as the resolution; return the gather's
// awaiter for the caller to chain. A gather that has
// already settled takes nothing more, and the failure is
// the parked task's own to raise.
Some(gather_id) => {
let HeapReadOutput::GatherFuture(mut gather_rd) = heap.read(gather_id) else {
panic!("gather_id doesn't point to a GatherFuture")
};
let outer_awaiter = gather_rd.fail(heap, error);
// Dropped before the cancel below: that releases the
// task's `GatherSlot` inc_ref on this gather, which
// must not run under a live reader on it.
drop(gather_rd);
match outer_awaiter {
// The gather handed the failure outwards, so
// nothing will ever deliver it to `task_id`. Its
// `await` raised, so it is finished — drop it, or
// it stays `Blocked` forever on a future that was
// just failed and unregistered, holding its
// coroutine and this gather alive for the rest of
// the session. Siblings are untouched: the task is
// blocked on an external future, so the cancel
// walk finds no gather to cascade into.
Some(outer) => {
self.cancel_task(task_id, heap);View on GitHub (pinned to adc986b362)
Solutions
- File a bug with the async reproducer
- Audit GatherFuture free/dec_ref ordering (the code deliberately drops the reader before releasing the GatherSlot inc_ref — preserve that ordering in edits)
- Run async/gather tests with memory-model-checks enabled
Example fix
// not applicable — internal scheduler invariant
Defensive patterns
Strategy: fallback
Try / catch
// Internal panic — no user-side catch; isolate and report.
match scheduler.fail_for_call(call_id, &err, heap) {
Some(awaiter) => chain(awaiter),
None => (),
} Prevention
- Keep GatherSlot inc_ref/dec_ref bookkeeping balanced
- Drop heap readers before releasing gather inc_refs (as the surrounding code does)
- Run gather tests with memory-model-checks after lifecycle edits
When it happens
Trigger: `fail_for_call` unwinding an awaiter chain that reaches an `Awaiter::Task` whose linked gather HeapId resolves to a non-GatherFuture heap entry; via heap corruption or a gather-lifecycle bug.
Common situations: Developing/fuzzing asyncio-style gather support; patches that free a GatherFuture while tasks still hold `GatherSlot` references to it.
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
- pending_externals entry doesn't point to an ExternalFuture
- fail_for_call: future was already resolved
- gather commit frame id is not a GatherFuture
- gather item is not a Coroutine, ExternalFuture, or GatherFut
- task coroutine_id doesn't point to a Coroutine
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/354b149db6c42c71.
Report an issue: GitHub.