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
resolve_future looks up the future id from pending_externals and expects an ExternalFuture heap entry; a different type panics. pending_externals should only ever contain ids of ExternalFutures, so this indicates bookkeeping corruption or slot reuse. Not triggerable from user code.
Source
Thrown at crates/monty/src/bytecode/vm/async_exec.rs:780
/// `Resolved(value)`, and delivers `value` to the awaiter (if any).
pub fn resolve_future(&mut self, call_id: u32, value: Value) {
let call_id = CallId::new(call_id);
let Some(future_id) = self.scheduler.take_pending_external(call_id) else {
value.drop_with(self);
return;
};
// Ensure future cleaned up on all paths
let fut_val = Value::Ref(future_id);
let this = self;
defer_drop!(fut_val, this);
let mut value_guard = DropGuard::new(value, this);
let (value, this) = value_guard.as_parts_mut();
let HeapReadOutput::ExternalFuture(mut fut) = this.heap.read(future_id) else {
panic!("pending_externals entry doesn't point to an ExternalFuture")
};
let awaiter_and_value = match &mut fut.get_mut(this.heap).state {
ExternalFutureState::Pending { awaiter } => awaiter.take().map(|a| (a, value.clone_with_heap(this.heap))),
ExternalFutureState::Resolved(_) | ExternalFutureState::Failed(_) => {
panic!("resolve_future: future was already resolved")
}
};
let (value, this) = value_guard.into_parts();
fut.get_mut(this.heap).state = ExternalFutureState::Resolved(value);
if let Some((awaiter, value)) = awaiter_and_value {
this.deliver_awaiter_success(awaiter, value);
}
}
/// Pushes `value` onto `task_id`'s stack and marks it ready. If the taskView on GitHub (pinned to adc986b362)
Solutions
- Ensure pending_externals entries are removed exactly when the future is resolved/dropped, not earlier
- Check no code dec_refs an ExternalFuture while its id remains in pending_externals
- Assert the heap type when inserting into pending_externals and on removal
- Report with a repro script if hit without local changes
Defensive patterns
Strategy: fallback
Prevention
- Treat pending_externals as owning its ids until resolution completes
- Remove entries exactly once, at resolution time
- Type-assert on insertion into pending_externals
When it happens
Trigger: The host resolves a pending external future (resume_with_resolved_futures → resolve_future) and the stored id resolves to a non-ExternalFuture heap entry — stale id after entry freed and slot reused.
Common situations: Encountered while modifying external-future resolution or heap reuse in async_exec.rs; possibly after changing how pending_externals entries are removed.
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 coroutine_id doesn't point to a Coroutine
- task has no frames and no coroutine_id
- resolve_future: future was already resolved
- Awaiter::GatherSlot gather id is not a GatherFuture
- current task is in unexpected Completed state after resolvin
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/8a172853cd1c899f.
Report an issue: GitHub.