pydantic/monty · error
checked above
Error message
checked above
What it means
This `unreachable!()` in `MountTable`-backed session resume (`resume_from_mounts`) asserts that when a `MountCallOutcome::NotHandled` arrives, `self.pending` is still the `Pending::Call` variant holding the suspended `os_call`. The variant was checked earlier in the same method, so firing indicates the pending state was mutated between the check and this arm — a broken state machine invariant in `monty-pool`. It is unreachable for library users driving the pool through its public API.
Source
Thrown at crates/monty-pool/src/checkout.rs:806
Ok(obj) => ResumeValue::Return(obj),
Err(err) => ResumeValue::Error(err.into_exception()),
};
match self.resume(value, &mut *on_print).await {
// The result never reached the child (too large or too deep
// to encode), so the call is still suspended — answer it
// with that error instead, letting the sandbox raise a
// catchable exception rather than stranding the feed. Only
// a rejection before the frame is written leaves `pending`
// set, so this cannot catch a genuine sandbox exception.
Err(PoolError::Runtime(exc)) if self.pending.is_some() => {
self.resume(ResumeValue::Error(exc), on_print).await.map(Some)
}
other => other.map(Some),
}
}
MountCallOutcome::NotHandled(call) => {
let Some(Pending::Call { os_call, .. }) = &mut self.pending else {
unreachable!("checked above");
};
*os_call = Some(Box::new(call));
Ok(None)
}
}
}
/// Answers a [`TurnEvent::NameLookup`] with a [`NameLookupResult`] (or a
/// `MontyObject`, an `Option<MontyObject>` where `None` is `Undefined`, or
/// a `MontyException` for `Error`): a value resolves the name; `Undefined`
/// makes the sandbox raise `NameError` for a plain lookup, or
/// `AttributeError` when the lookup carried an `object_id` (a lazy
/// attribute on a host-backed object — a class instance or class type);
/// `Error` raises the host's exception in the sandbox, bypassing
/// `hasattr()` / `getattr()` defaults the way a raising property does.
pub async fn resume_name_lookup(
&mut self,
result: impl Into<NameLookupResult>,View on GitHub (pinned to adc986b362)
Solutions
- Re-read `self.pending` immediately before the `NotHandled` handling instead of relying on an earlier check
- Audit the method for any path that replaces or clears `self.pending` between the first match and this arm
- Replace the assertion with a logged internal-error / crashed-worker result so a latent bug cannot panic the parent pool
- Run `cargo test -p monty-pool` including crash/recovery tests
Example fix
// before
let Some(Pending::Call { os_call, .. }) = &mut self.pending else {
unreachable!("checked above");
};
// after
let Some(Pending::Call { os_call, .. }) = &mut self.pending else {
return Err(PoolError::internal(
"pending turn is not a call when handling NotHandled"));
}; Defensive patterns
Strategy: type-guard
Validate before calling
// Check pending state right before use, not earlier
if !matches!(self.pending, Some(Pending::Call { .. })) {
return Err(PoolError::internal("pending turn is not a call"));
} Type guard
fn pending_call(pending: &Option<Pending>) -> Option<&OsCall> {
match pending {
Some(Pending::Call { os_call, .. }) => os_call.as_ref(),
_ => None,
}
} Try / catch
// Replace the panic with a recoverable internal error so the pool can replace the worker
let Some(Pending::Call { os_call, .. }) = &mut self.pending else {
return Err(PoolError::internal("NotHandled with no pending call"));
}; Prevention
- Re-verify invariants at the point of use rather than trusting earlier checks in long methods
- Model the pending state as an enum consumed by value (`std::mem::take`) so the compiler enforces single-handling
- Add pool tests that cover NotHandled outcomes for every pending-turn kind
- Never mutate `self.pending` between the initial dispatch and outcome handling
When it happens
Trigger: Only when code modifies `self.pending` (e.g. resolving/clearing a pending call) between the earlier `Pending::Call` check and the `NotHandled` arm, or when a new `Pending` variant is introduced without updating this method.
Common situations: Hit during development of `monty-pool`: adding new pending-turn kinds (timers, futures), refactoring the suspension/resume state machine, or reordering the outcome match arms.
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
- gather commit frame id is not a GatherFuture
- gather item is not a Coroutine, ExternalFuture, or GatherFut
- varargs/varkwargs own no param slot
- ObjectList is decode-only
- PairList is decode-only
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/978e37c1f364d424.
Report an issue: GitHub.