ianstormtaylor/slate · error · Error

Cannot apply a "merge_node" operation at path

Error message

Cannot apply a "merge_node" operation at path [${path}] to nodes of different interfaces: ${Scrubber.stringify(
                node
              )} ${Scrubber.stringify(prev)}

What it means

merge_node merges a node into its previous sibling, which only works when both are text nodes or both are element nodes. It throws when one is a text node and the other an element (or otherwise incompatible interfaces), since there is no defined way to merge them.

Solutions

  1. Before merging, check that Node.isText matches for both the node and its previous sibling; if they differ, use Transforms.wrapNodes/unwrapNodes or remove one instead.
  2. Run your editor with a schema/normalizer so parents only ever contain homogeneous mergeable children.
  3. Validate the document structure after deserializing HTML/Markdown before applying merges.

Example fix

// before
Transforms.mergeNodes(editor, { at: path })

// after
const node = Node.get(editor, path)
const prev = Node.get(editor, Path.previous(path))
if (Node.isText(node) === Node.isText(prev)) {
  Transforms.mergeNodes(editor, { at: path })
} else {
  Transforms.removeNodes(editor, { at: path })
}
Defensive patterns

Strategy: type-guard

Validate before calling

const node = Node.get(editor, path)
const prev = Node.get(editor, Path.previous(path))
if (Node.isText(node) === Node.isText(prev)) {
  Transforms.mergeNodes(editor, { at: path })
}

Type guard

function areMergeable(a: Node, b: Node): boolean {
  return (Node.isText(a) && Node.isText(b)) || (Node.isElement(a) && Node.isElement(b))
}

Try / catch

try {
  Transforms.mergeNodes(editor, { at: path })
} catch (e) {
  if (e instanceof Error && e.message.includes('different interfaces')) {
    Transforms.removeNodes(editor, { at: path })
  } else throw e
}

Prevention

When it happens

Trigger: Applying merge_node at a path whose node is a text node while the previous sibling is an element, or vice versa — e.g. merging path [1] where [0] is an element and [1] is a text node; often from hand-built operations or a document whose children violate Slate's content schema.

Common situations: Documents with mixed invalid children (element containing both element and text children directly where schema forbids it); custom normalizers that merge nodes without checking node types; replaying operations against a diverged document.

Related errors


AI-assisted analysis of ianstormtaylor/slate@72a37c701e (2026-08-27). Data as JSON: /api/errors/218273dfcd5a6202. Report an issue: GitHub.

Appendix: source

Thrown at packages/slate/src/interfaces/transforms/general.ts:117

            `Cannot apply a "merge_node" operation at path [${path}] because the root node cannot be merged.`
          )
        }

        // Defend against malicious paths containing strings
        if (typeof index !== 'number' || typeof prevIndex !== 'number')
          throw new Error('Index must be number')

        modifyChildren(editor, Path.parent(path), children => {
          const node = children[index]
          const prev = children[prevIndex]
          let newNode: Descendant

          if (Node.isText(node) && Node.isText(prev)) {
            newNode = { ...prev, text: prev.text + node.text }
          } else if (Node.isElement(node) && Node.isElement(prev)) {
            newNode = { ...prev, children: prev.children.concat(node.children) }
          } else {
            throw new Error(
              `Cannot apply a "merge_node" operation at path [${path}] to nodes of different interfaces: ${Scrubber.stringify(
                node
              )} ${Scrubber.stringify(prev)}`
            )
          }

          return replaceChildren(children, prevIndex, 2, newNode)
        })

        transformSelection = true
        break
      }

      case 'move_node': {
        const { path, newPath } = op
        const index = path[path.length - 1]

        if (Path.isAncestor(path, newPath)) {

View on GitHub (pinned to 72a37c701e)