pydantic/monty · critical
pending_externals entry doesn't point to an ExternalFuture
Error message
pending_externals entry doesn't point to an ExternalFuture
What it means
In the async scheduler, `fail_for_call` removes the ExternalFuture registered for a failing external call; this panic fires if the heap entry for that future's HeapId is not an `ExternalFuture`. The `pending_externals` map is only populated with future HeapIds the scheduler itself allocated, so this indicates heap corruption or a scheduler bug — it cannot be triggered by host callback code alone.
Source
Thrown at crates/monty/src/bytecode/vm/scheduler.rs:481
/// with a clone of the error, and yields the awaiter that owned the
/// future's `Pending` slot — except for `Awaiter::Task(t)` where `t` is a
/// child of a still-running gather: in that case we settle the gather here
/// (rather than leaving the parked task `Failed` for a sibling's
/// resolution to discover later) and return the gather's awaiter instead,
/// so the caller's chain walk picks up at the right level.
///
/// The returned `Awaiter` is owned (callers must walk it via
/// `deliver_awaiter_failure`, which drops every link).
///
/// Returns `None` when there's nothing to propagate (unknown CallId,
/// already-resolved future, or the future had no awaiter — the failure
/// is simply cached on the future for replay).
#[must_use]
pub fn fail_for_call(&mut self, call_id: CallId, error: &RunError, heap: &mut HeapReader<'_>) -> Option<Awaiter> {
let future_id = self.pending_externals.remove(&call_id)?;
let HeapReadOutput::ExternalFuture(mut fut) = heap.read(future_id) else {
panic!("pending_externals entry doesn't point to an ExternalFuture")
};
let awaiter = match mem::replace(&mut fut.get_mut(heap).state, ExternalFutureState::Failed(error.clone())) {
ExternalFutureState::Pending { awaiter } => awaiter,
ExternalFutureState::Resolved(_) | ExternalFutureState::Failed(_) => {
panic!("fail_for_call: future was already resolved")
}
};
drop(fut);
heap.dec_ref(future_id);
match awaiter {
// Nothing is waiting on this call — it was never awaited. The
// failure stays cached on the future for a later await to replay.
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.View on GitHub (pinned to adc986b362)
Solutions
- File a bug including the failing external-call sequence
- Audit ExternalFuture drop/cancel paths for frees that leave a stale call_id in `pending_externals`
- Run async test suites with memory-model-checks to catch premature frees
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 => (),
} // if it panics, file a scheduler bug with the call sequence Prevention
- Never free an ExternalFuture while its call_id remains in pending_externals
- Run async scheduler tests with memory-model-checks
- Log call_id lifecycle when debugging the scheduler
When it happens
Trigger: Calling `fail_for_call(call_id, error, heap)` for a call_id whose `pending_externals` entry points at a heap entry that is not an ExternalFuture; only via heap corruption or a scheduler/allocation bug.
Common situations: Developing or fuzzing the async/external-call scheduler; patches that free an ExternalFuture while its call_id is still pending.
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
- fail_for_call: future was already resolved
- gather_id doesn't point to a GatherFuture
- task coroutine_id doesn't point to a Coroutine
- task has no frames and no coroutine_id
- pending_externals entry doesn't point to an ExternalFuture
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/d38ad52d3a509986.
Report an issue: GitHub.