{"record":{"id":"d38ad52d3a509986","repo":"pydantic/monty","slug":"pending-externals-entry-doesn-t-point-to-an-externalfuture-d38ad5","errorCode":null,"errorMessage":"pending_externals entry doesn't point to an ExternalFuture","messagePattern":"pending_externals entry doesn't point to an ExternalFuture","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/monty/src/bytecode/vm/scheduler.rs","lineNumber":481,"sourceCode":"    /// with a clone of the error, and yields the awaiter that owned the\n    /// future's `Pending` slot — except for `Awaiter::Task(t)` where `t` is a\n    /// child of a still-running gather: in that case we settle the gather here\n    /// (rather than leaving the parked task `Failed` for a sibling's\n    /// resolution to discover later) and return the gather's awaiter instead,\n    /// so the caller's chain walk picks up at the right level.\n    ///\n    /// The returned `Awaiter` is owned (callers must walk it via\n    /// `deliver_awaiter_failure`, which drops every link).\n    ///\n    /// Returns `None` when there's nothing to propagate (unknown CallId,\n    /// already-resolved future, or the future had no awaiter — the failure\n    /// is simply cached on the future for replay).\n    #[must_use]\n    pub fn fail_for_call(&mut self, call_id: CallId, error: &RunError, heap: &mut HeapReader<'_>) -> Option<Awaiter> {\n        let future_id = self.pending_externals.remove(&call_id)?;\n\n        let HeapReadOutput::ExternalFuture(mut fut) = heap.read(future_id) else {\n            panic!(\"pending_externals entry doesn't point to an ExternalFuture\")\n        };\n        let awaiter = match mem::replace(&mut fut.get_mut(heap).state, ExternalFutureState::Failed(error.clone())) {\n            ExternalFutureState::Pending { awaiter } => awaiter,\n            ExternalFutureState::Resolved(_) | ExternalFutureState::Failed(_) => {\n                panic!(\"fail_for_call: future was already resolved\")\n            }\n        };\n        drop(fut);\n        heap.dec_ref(future_id);\n\n        match awaiter {\n            // Nothing is waiting on this call — it was never awaited. The\n            // failure stays cached on the future for a later await to replay.\n            None => None,\n            Some(Awaiter::Task(task_id)) => {\n                // A task's own awaiter is the `GatherSlot` its gather gave it;\n                // borrow that gather's id without taking the task's ref, which\n                // stays until the task is cancelled.","sourceCodeStart":463,"sourceCodeEnd":499,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty/src/bytecode/vm/scheduler.rs#L463-L499","documentation":"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.","triggerScenarios":"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.","commonSituations":"Developing or fuzzing the async/external-call scheduler; patches that free an ExternalFuture while its call_id is still pending.","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"],"exampleFix":"// not applicable — internal scheduler invariant","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Internal panic — no user-side catch; isolate and report.\nmatch scheduler.fail_for_call(call_id, &err, heap) {\n    Some(awaiter) => chain(awaiter),\n    None => (),\n} // if it panics, file a scheduler bug with the call sequence","preventionTips":["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"],"tags":["panic","internal","async","scheduler","futures"],"backgroundTag":"internal-invariant-violation","analyzedSha":"adc986b362e3961f407868cb118a99fe831b9e61","analyzedAt":"2026-09-13T19:19:18.698Z","contentChangedAt":"2026-09-13T19:19:18.698Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}