Hmbown/CodeWhale · error

config changed after it was loaded; reload

Error message

config changed after it was loaded; reload {} and retry instead of overwriting concurrent changes

What it means

Optimistic-concurrency guard in replace_config_document_if_unchanged. Under the config write lock the file is re-read and compared against the `expected` bytes the caller loaded earlier; if the on-disk content differs, another writer changed the config and this save is refused instead of silently overwriting the concurrent change.

Solutions

  1. Reload the config document from disk and re-apply your change, then save again.
  2. Merge your intended edits with the concurrent changes before retrying.
  3. Avoid editing the config file manually while the app is running.
  4. If the mismatch is expected, pass the current on-disk content as `expected` deliberately.

Example fix

// before (stale snapshot)
let doc = ConfigDocument::load(&path)?;
// ... long work ...
doc.replace_config_document_if_unchanged(&expected, &new_body)?; // fails if file moved
// after (reload just before save)
let doc = ConfigDocument::load(&path)?; // fresh load immediately before replace
let expected = doc.raw_bytes().to_vec();
doc.replace_config_document_if_unchanged(&expected, &new_body)?;
Defensive patterns

Strategy: retry

Validate before calling

// Compare current on-disk bytes to your snapshot before saving
let current = std::fs::read(&path).ok();
if current.as_deref() != Some(expected.as_slice()) {
    eprintln!("config on disk changed since load; reload and merge before saving");
}

Try / catch

match doc.replace_config_document_if_unchanged(&expected, &new_body) {
    Err(e) if e.to_string().contains("config changed after it was loaded") => {
        // reload, re-apply your edit on the fresh document, retry once
        let doc = ConfigDocument::load(&path)?;
        let fresh = doc.raw_bytes().to_vec();
        let merged = merge_my_changes(&fresh, &my_edit);
        doc.replace_config_document_if_unchanged(&fresh, &merged)?
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling save/create_config_document_if_unchanged with an `expected` snapshot that no longer matches the file on disk — e.g. two CLI instances or an external editor modified the config between load and write.

Common situations: Running two codewhale instances concurrently; editing config.toml in an editor while the app writes settings; provisioning scripts writing the file during a tool-driven change.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/ddf1b42118beb8a1. Report an issue: GitHub.

Appendix: source

Thrown at crates/config/src/config_document.rs:111

/// acquired. This closes the `exists()`/create race in first-run writers.
pub fn create_config_document(path: &Path, body: &str) -> Result<()> {
    replace_config_document_if_unchanged(path, None, body)
}

/// Replace a full typed snapshot only when on-disk bytes still equal the
/// snapshot the caller originally loaded. `None` means the file was absent.
pub fn replace_config_document_if_unchanged(
    path: &Path,
    expected: Option<&str>,
    body: &str,
) -> Result<()> {
    with_config_write_lock(path, |path| {
        let current = read_optional_config(path)?;
        if current.as_deref() == Some(body) {
            return Ok(());
        }
        if current.as_deref() != expected {
            bail!(
                "config changed after it was loaded; reload {} and retry instead of overwriting concurrent changes",
                crate::quote_os_path(path)
            );
        }
        persist_locked(path, current.as_deref(), body.as_bytes())
    })
}

/// Set a value at `segments`, creating implicit parent tables while preserving
/// existing key/value decor.
pub fn set_config_document_value(
    doc: &mut toml_edit::DocumentMut,
    segments: &[&str],
    value: impl Into<toml_edit::Value>,
) -> Result<()> {
    let (key, parents) = segments
        .split_last()
        .context("config value path must not be empty")?;

View on GitHub (pinned to 73e0f67d83)