rust-lang/cargo · error
why did we save a frame that has no next?
Error message
why did we save a frame that has no next?
What it means
Resolver invariant panic in find_candidate (src/resolver/mod.rs:1001). After popping a BacktrackFrame off backtrack_stack, cargo calls frame.remaining_candidates.next(...) and .expect("why did we save a frame that has no next?"). Unlike a debug_assert, this .expect runs in release builds too. The contract being defended: a frame must only be pushed onto backtrack_stack when it still has at least one untried candidate. If next() returns None here, the backtracking bookkeeping pushed an exhausted frame.
Source
Thrown at src/resolver/mod.rs:1001
}
trace!(
"{} = \"{}\" skip as not solving {}: {:?}",
frame.dep.package_name(),
frame.dep.version_req(),
parent.package_id(),
conflicting_activations
);
}
} else {
// If we're here then we are in abnormal situations and need to just go one frame at a time.
new_frame = backtrack_stack.pop();
}
new_frame.map(|mut frame| {
let (candidate, has_another) = frame
.remaining_candidates
.next(&mut frame.conflicting_activations, &frame.context)
.expect("why did we save a frame that has no next?");
(candidate, has_another, frame)
})
}
fn check_cycles(resolve: &Resolve) -> CargoResult<()> {
// Perform a simple cycle check by visiting all nodes.
// We visit each node at most once and we keep
// track of the path through the graph as we walk it. If we walk onto the
// same node twice that's a cycle.
let mut checked = HashSet::with_capacity_and_hasher(resolve.len(), FxBuildHasher::default());
let mut path = Vec::with_capacity(4);
let mut visited = HashSet::with_capacity_and_hasher(4, FxBuildHasher::default());
for pkg in resolve.iter() {
if !checked.contains(&pkg) {
visit(&resolve, pkg, &mut visited, &mut path, &mut checked)?
}
}
return Ok(());View on GitHub (pinned to 0e07a15537)
Solutions
- Check the cargo version / git commit: downgrade to the last known-good stable cargo (rustup install stable) and retry; if it resolves, file a rust-lang/cargo regression issue with the manifest.
- Minimize the workspace (remove workspace members / optional deps / patch sections) until the panic disappears to isolate the trigger, then report it.
- If you are building cargo from source, add logging around backtrack_stack.push / the point where remaining_candidates is constructed to find which push stored an exhausted iterator.
- As a local workaround, run with `cargo generate-lockfile` after temporarily relaxing or pinning the dependency that initiates the failing backtrack.
Example fix
// before: cargo 1.x panics during resolution // thread 'main' panicked at src/resolver/mod.rs:1001: why did we save a frame that has no next? // after: isolate then report // rustup toolchain install 1.STABLE && cargo +1.STABLE generate-lockfile // # then file an issue with the minimized Cargo.toml that still panics on nightly
Defensive patterns
Strategy: validation
Validate before calling
// Caller cannot inspect BacktrackFrame.remaining_candidates directly.
// Validate the environment instead: confirm a known-good stable toolchain is in use
// before invoking resolution, so a regression that trips this panic is bypassed.
fn ensure_stable_cargo() -> std::io::Result<()> {
let out = std::process::Command::new("cargo").arg("--version").output()?;
let v = String::from_utf8_lossy(&out.stdout);
assert!(v.contains("stable") || !v.contains("nightly"), "avoid toolchains with the exhausted-frame regression");
Ok(())
} Type guard
// No type guard: this is an internal Option::expect on a private iterator. // The only 'narrowing' is choosing a cargo version whose find_candidate invariant holds.
Try / catch
use std::panic;
let outcome = panic::catch_unwind(|| {
// resolve() / generate-lockfile call that may hit find_candidate's expect
});
match outcome {
Ok(resolve) => { /* use resolution */ }
Err(_) => { /* switch toolchain / simplify manifest; report upstream */ }
} Prevention
- Pin a known-good stable cargo via rustup for CI that performs resolution.
- Minimize optional dependencies and patch-table complexity in large workspaces to avoid exotic backtrack paths.
- If you maintain cargo, regression-test push sites of backtrack_stack to confirm remaining_candidates is non-empty before saving.
When it happens
Trigger: Any resolution that backtracks (either the normal cx.is_conflicting-guided pop loop or the abnormal one-frame-at-a-time fallback branch at line 992-994) and then pops a frame whose remaining_candidates iterator yields None on the first .next(). Typically reached after a prior backtrack left a stale/exhausted frame on the stack.
Common situations: A new cargo version with a regression in how BacktrackFrame.remaining_candidates is advanced before pushing; rare dependency graphs (lots of optional deps + feature unification + yanked versions) that exercise unusual backtrack paths. Almost always an internal cargo bug rather than a user manifest error.
Related errors
- parent not currently active!?
- an already used dep now pending!?
- not currently active!?
- We've already checked that there is exactly one.
- source ID should have valid URLs
AI-assisted analysis of rust-lang/cargo@0e07a15537 (2026-08-06).
Data as JSON: /data/errors/b69ef7849836a837.json.
Report an issue: GitHub.