Textualize/textual · error · HistoryException

Cannot add an edit to history before it has been performed…

Error message

Cannot add an edit to history before it has been performed using `Edit.do`.

What it means

History.record raises HistoryException when asked to record an Edit that has not been performed yet — the edit carries no _edit_result because Edit.do was never called. History only accepts edits that actually happened, so it can store the replaced text for undo.

Solutions

  1. Call edit.do(document) before history.record(edit)
  2. Or use the higher-level Document.edit API, which performs and records atomically
  3. Skip recording for no-op edits (record already ignores empty edits after do)

Example fix

# before
edit = Edit(from_loc, to_loc, text)
history.record(edit)  # raises
# after
edit = Edit(from_loc, to_loc, text)
edit.do(document)
history.record(edit)
Defensive patterns

Strategy: validation

Validate before calling

def perform_and_record(history, edit, document):
    if edit._edit_result is None:
        edit.do(document)
    history.record(edit)

Try / catch

from textual.document._history import HistoryException
try:
    history.record(edit)
except HistoryException:
    edit.do(document)
    history.record(edit)

Prevention

When it happens

Trigger: Creating an Edit(edit) and passing it to history.record(edit) directly without first calling edit.do(document). Typically hit when writing custom edit flows in TextArea/document code.

Common situations: Custom undo/redo integrations or scripted transformations where the developer builds the edit and records it manually, forgetting the do step.

Related errors


AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27). Data as JSON: /api/errors/d497aec378624ef8. Report an issue: GitHub.

Appendix: source

Thrown at src/textual/document/_history.py:78

        - The checkpoint timer expires.
        - The maximum number of characters permitted in a checkpoint is reached.
        - A redo is performed (we should not add new edits to a batch that has been redone).
        - The programmer has requested a new batch via a call to `force_new_batch`.
            - e.g. the TextArea widget may call this method in some circumstances.
            - Clicking to move the cursor elsewhere in the document should create a new batch.
            - Movement of the cursor via a keyboard action that is NOT an edit.
            - Blurring the TextArea creates a new checkpoint.
        - The current edit involves a deletion/replacement and the previous edit did not.
        - The current edit is a pure insertion and the previous edit was not.
        - The edit involves insertion or deletion of one or more newline characters.
        - An edit which inserts more than a single character (a paste) gets an isolated batch.

        Args:
            edit: The edit to record.
        """
        edit_result = edit._edit_result
        if edit_result is None:
            raise HistoryException(
                "Cannot add an edit to history before it has been performed using `Edit.do`."
            )

        if edit.text == "" and edit_result.replaced_text == "":
            return None

        is_replacement = bool(edit_result.replaced_text)
        undo_stack = self._undo_stack
        current_time = self._get_time()
        edit_characters = len(edit.text)
        contains_newline = "\n" in edit.text or "\n" in edit_result.replaced_text

        # Determine whether to create a new batch, or add to the latest batch.
        if (
            not undo_stack
            or self._force_end_batch
            or edit_characters > 1
            or contains_newline

View on GitHub (pinned to 06dbeef4bb)