{"id":"b69ef7849836a837","repo":"rust-lang/cargo","slug":"why-did-we-save-a-frame-that-has-no-next","errorCode":null,"errorMessage":"why did we save a frame that has no next?","messagePattern":"why did we save a frame that has no next\\?","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/resolver/mod.rs","lineNumber":1001,"sourceCode":"            }\n            trace!(\n                \"{} = \\\"{}\\\" skip as not solving {}: {:?}\",\n                frame.dep.package_name(),\n                frame.dep.version_req(),\n                parent.package_id(),\n                conflicting_activations\n            );\n        }\n    } else {\n        // If we're here then we are in abnormal situations and need to just go one frame at a time.\n        new_frame = backtrack_stack.pop();\n    }\n\n    new_frame.map(|mut frame| {\n        let (candidate, has_another) = frame\n            .remaining_candidates\n            .next(&mut frame.conflicting_activations, &frame.context)\n            .expect(\"why did we save a frame that has no next?\");\n        (candidate, has_another, frame)\n    })\n}\n\nfn check_cycles(resolve: &Resolve) -> CargoResult<()> {\n    // Perform a simple cycle check by visiting all nodes.\n    // We visit each node at most once and we keep\n    // track of the path through the graph as we walk it. If we walk onto the\n    // same node twice that's a cycle.\n    let mut checked = HashSet::with_capacity_and_hasher(resolve.len(), FxBuildHasher::default());\n    let mut path = Vec::with_capacity(4);\n    let mut visited = HashSet::with_capacity_and_hasher(4, FxBuildHasher::default());\n    for pkg in resolve.iter() {\n        if !checked.contains(&pkg) {\n            visit(&resolve, pkg, &mut visited, &mut path, &mut checked)?\n        }\n    }\n    return Ok(());","sourceCodeStart":983,"sourceCodeEnd":1019,"githubUrl":"https://github.com/rust-lang/cargo/blob/0e07a155371a6ce88ae53a2c00df940280c09a67/src/resolver/mod.rs#L983-L1019","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: cargo 1.x panics during resolution\n//   thread 'main' panicked at src/resolver/mod.rs:1001: why did we save a frame that has no next?\n\n// after: isolate then report\n//   rustup toolchain install 1.STABLE && cargo +1.STABLE generate-lockfile\n//   # then file an issue with the minimized Cargo.toml that still panics on nightly","handlingStrategy":"validation","validationCode":"// Caller cannot inspect BacktrackFrame.remaining_candidates directly.\n// Validate the environment instead: confirm a known-good stable toolchain is in use\n// before invoking resolution, so a regression that trips this panic is bypassed.\nfn ensure_stable_cargo() -> std::io::Result<()> {\n    let out = std::process::Command::new(\"cargo\").arg(\"--version\").output()?;\n    let v = String::from_utf8_lossy(&out.stdout);\n    assert!(v.contains(\"stable\") || !v.contains(\"nightly\"), \"avoid toolchains with the exhausted-frame regression\");\n    Ok(())\n}","typeGuard":"// No type guard: this is an internal Option::expect on a private iterator.\n// The only 'narrowing' is choosing a cargo version whose find_candidate invariant holds.","tryCatchPattern":"use std::panic;\nlet outcome = panic::catch_unwind(|| {\n    // resolve() / generate-lockfile call that may hit find_candidate's expect\n});\nmatch outcome {\n    Ok(resolve) => { /* use resolution */ }\n    Err(_)     => { /* switch toolchain / simplify manifest; report upstream */ }\n}","preventionTips":["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."],"tags":["resolver","backtracking","panic","cargo-internal","rust"],"analyzedSha":"0e07a155371a6ce88ae53a2c00df940280c09a67","analyzedAt":"2026-08-06T01:46:58.334Z","schemaVersion":2}