ianstormtaylor/slate · critical · Error
Could not completely normalize the editor after ${maxIterati
Error message
Could not completely normalize the editor after ${maxIterations} iterations! This is usually due to incorrect normalization logic that leaves a node in an invalid state. What it means
Slate normalizes the editor after every change by repeatedly running normalizeNode over 'dirty' paths until the document is valid. shouldNormalize caps this loop at initialDirtyPathsLength * 42 iterations as a safety hack; if normalization keeps marking nodes dirty (e.g. a normalizeNode override that never converges, always returns true for shouldNormalize-like hooks, or a normalize function that re-dirties the same path), the cap is hit and this error throws.
Source
Thrown at packages/slate/src/core/should-normalize.ts:11
import { WithEditorFirstArg } from '../utils/types'
import { Editor } from '../interfaces/editor'
export const shouldNormalize: WithEditorFirstArg<Editor['shouldNormalize']> = (
editor,
{ iteration, initialDirtyPathsLength }
) => {
const maxIterations = initialDirtyPathsLength * 42 // HACK: better way?
if (iteration > maxIterations) {
throw new Error(
`Could not completely normalize the editor after ${maxIterations} iterations! This is usually due to incorrect normalization logic that leaves a node in an invalid state.`
)
}
return true
}
View on GitHub (pinned to 72a37c701e)
Solutions
- Audit every custom normalizeNode override: make each fix idempotent — only act when the invalid condition is present, and always fall through to the previous normalizeNode for non-matching nodes
- Ensure normalize fixes don't call transforms that re-dirty the same path every pass (e.g. removing and re-adding the same property)
- Log the dirty paths per iteration (wrap Editor.normalize or shouldNormalize) to find the ping-pong pair of normalizers
- Reduce the problem: disable custom normalizers one by one until the loop stops to identify the culprit
Example fix
// before
const { normalizeNode } = editor
editor.normalizeNode = entry => {
const [node] = entry
editor.normalizeNode(entry) // re-normalizes same entry every time -> infinite loop
if (node.type !== 'p') Transforms.setNodes(editor, { type: 'p' }, ...)
}
// after
const { normalizeNode } = editor
editor.normalizeNode = entry => {
const [node, path] = entry
if (Element.isElement(node) && node.type !== 'p') {
Transforms.setNodes(editor, { type: 'p' }, { at: path })
return
}
normalizeNode(entry)
} Defensive patterns
Strategy: validation
Validate before calling
const { normalizeNode } = editor
editor.normalizeNode = entry => {
const [node, path] = entry
let handled = false
// each fix must only run when the invalid state exists
if (Element.isElement(node) && node.type == null) {
Transforms.setNodes(editor, { type: 'paragraph' }, { at: path })
handled = true
}
if (!handled) normalizeNode(entry)
} Try / catch
try {
Editor.normalize(editor, { force: true })
} catch (e) {
if (/Could not completely normalize/.test(e.message)) {
// log dirty paths, find ping-pong normalizers
} else throw e
} Prevention
- Make every normalizeNode fix idempotent and conditional
- Always delegate unmatched nodes to the previous normalizeNode
- Never call transforms that re-dirty the same path during normalization
- Test plugins against documents that trigger multiple normalizers at once
When it happens
Trigger: A custom editor.normalizeNode override that fixes one thing but invalidates another (ping-pong normalization); withOverride/shouldNormalize plugins (e.g. slate-autoformat-style plugins) always returning true; a normalizeNode that calls editor.normalizeNode again or transforms nodes every pass; a normalize function that dirties the node it just normalized.
Common situations: Adding a normalization plugin whose fix triggers the next plugin's fix indefinitely; normalizations that insert nodes during normalization causing cascading dirty paths; upgrading Slate where normalizeNode chaining conventions changed (must call the previous normalizeNode for unmatched nodes).
Related errors
- Unable to find the path for Slate node: ${Scrubber.stringify
- Cannot resolve a Slate range from a DOM event: ${event}
- Could not set focus, editor seems stuck with pending operati
- Unable to find a host window element for this editor
- Cannot resolve a DOM node from Slate node: ${Scrubber.string
AI-assisted analysis of ianstormtaylor/slate@72a37c701e (2026-08-27).
Data as JSON: /api/errors/f1b979f0215695e9.
Report an issue: GitHub.