sinelaw/fresh · error

Buffer has unsaved changes

Error message

Buffer has unsaved changes

What it means

close_buffer refuses to close a buffer whose document has unsaved modifications. buffer_close.rs:44 checks state.buffer.is_modified() for the buffer in the active window and returns this error instead of discarding edits. It's a deliberate safety gate; close_buffer_internal (or force-close variants) bypass it.

Solutions

  1. Save the buffer first (or prompt the user to save/discard) before closing
  2. Use the force-close API that skips the modified check when discarding edits is intended
  3. Check buffer.is_modified() before calling close_buffer and branch to a save/discard dialog

Example fix

// before
editor.close_buffer(id)?; // errors when dirty
// after
if editor.is_buffer_modified(id)? {
    editor.save_buffer(id)?; // or prompt user
}
editor.close_buffer(id)?;
Defensive patterns

Strategy: validation

Validate before calling

if editor.is_buffer_modified(id)? {
    // prompt: Save / Discard / Cancel
    prompt_save_before_close(id)?;
}

Try / catch

match editor.close_buffer(id) {
    Err(e) if e.to_string() == "Buffer has unsaved changes" => {
        editor.save_buffer(id)?;
        editor.close_buffer(id)?;
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling close_buffer (directly or via close_tab_in_split, preview_file, dismiss_preview, handle_close_buffer, handle_close_terminal) while the target buffer's modified flag is set.

Common situations: User closes a tab with unsaved edits and no save-prompt path ran; scripted/automated tab cleanup hitting dirty buffers; preview close paths when the preview buffer was edited.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13). Data as JSON: /api/errors/8a92cd0ea1b12240. Report an issue: GitHub.

Appendix: source

Thrown at crates/fresh-editor/src/app/buffer_close.rs:44

    /// Set when the LRU landing target was a buffer *group* rather than a
    /// buffer: `buffer` is then only housekeeping and the caller re-activates
    /// the group tab on this leaf.
    return_to_group: Option<LeafId>,
}

impl Editor {
    /// Close the given buffer
    pub fn close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
        // Check for unsaved changes
        if let Some(state) = self
            .windows
            .get(&self.active_window)
            .map(|w| &w.buffers)
            .expect("active window present")
            .get(&id)
        {
            if state.buffer.is_modified() {
                return Err(anyhow::anyhow!("Buffer has unsaved changes"));
            }
        }
        self.close_buffer_internal(id, false)
    }

    /// Force close the given buffer without checking for unsaved changes
    /// Use this when the user has already confirmed they want to discard changes
    pub fn force_close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
        self.close_buffer_internal(id, false)
    }

    /// Close the last editor tab while a Utility Dock is present, keeping the
    /// editor leaf alive but empty (issue #2283). The editor leaf we intend to
    /// keep is made the active split so the synthesized placeholder buffer
    /// lands there rather than in the dock, then the closing buffer is torn
    /// down with `force_empty_placeholder` so no dock terminal gets adopted.
    pub(crate) fn close_tab_keeping_editor_leaf_empty(
        &mut self,

View on GitHub (pinned to 67894ca546)