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

  1. 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.
  2. Minimize the workspace (remove workspace members / optional deps / patch sections) until the panic disappears to isolate the trigger, then report it.
  3. 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.
  4. 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

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


AI-assisted analysis of rust-lang/cargo@0e07a15537 (2026-08-06). Data as JSON: /data/errors/b69ef7849836a837.json. Report an issue: GitHub.