ianstormtaylor/slate · error · Error

Index must be number

Error message

Index must be number

What it means

A defensive check inside merge_node: the final index of the path (and of the previous sibling's path) must be a number. It throws when the operation's path contains a string segment, as can happen with deserialized or maliciously crafted operations.

Source

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

        transformSelection = true
        break
      }

      case 'merge_node': {
        const { path } = op
        const index = path[path.length - 1]
        const prevPath = Path.previous(path)
        const prevIndex = prevPath[prevPath.length - 1]

        if (path.length === 0) {
          throw new Error(
            `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)}`
            )
          }

View on GitHub (pinned to 72a37c701e)

Solutions

  1. Validate/normalize operation paths before applying: ensure every segment is a number.
  2. Sanitize deserialized operations with a schema check (e.g. zod) that coerces or rejects string segments.
  3. Fix the code building the path — avoid template literals that yield strings.

Example fix

// before
const op = JSON.parse(saved) // path segments may be strings
Editor.apply(editor, op)

// after
const op = JSON.parse(saved)
if (!op.path?.every((n: unknown) => typeof n === 'number')) {
  throw new Error('Invalid operation path')
}
Editor.apply(editor, op)
Defensive patterns

Strategy: validation

Validate before calling

const valid = op.path.every((n: unknown) => typeof n === 'number')
if (valid) Editor.apply(editor, op)

Type guard

function isNumericPath(path: unknown[]): path is Path {
  return path.every(n => typeof n === 'number' && Number.isInteger(n) && n >= 0)
}

Try / catch

try {
  Editor.apply(editor, op)
} catch (e) {
  if (e instanceof Error && e.message === 'Index must be number') {
    // reject/sanitize the incoming operation
  } else throw e
}

Prevention

When it happens

Trigger: Applying a merge_node whose path contains non-numeric segments, e.g. path: ['a', 1]; typically from JSON.parse'd operations from untrusted sources, deserialized drafts, or a bug building paths from template strings.

Common situations: Loading operation logs from localStorage/backend where a path got stringified; accepting operations from a collaborative peer without validation; constructing paths with template literals ('0' instead of 0).

Related errors


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