{"record":{"id":"6fecc38fa70af87f","repo":"BookStackApp/BookStack","slug":"error-tostring-parent-dom-tagname-new-ch","errorCode":null,"errorMessage":"${error.toString()} Parent: ${dom.tagName}, new child: {tag: ${replacementDOM.tagName} key: ${nextFrstChildKey}}, old child: {tag: ${lastDOM.tagName}, key: ${prevFirstChildKey}}.","messagePattern":"(.+?) Parent: (.+?), new child: (.+?), old child: (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"resources/js/wysiwyg/lexical/core/LexicalReconciler.ts","lineNumber":378,"sourceCode":"    const prevFirstChildKey = prevElement.__first as NodeKey;\n    const nextFrstChildKey = nextElement.__first as NodeKey;\n    if (prevFirstChildKey === nextFrstChildKey) {\n      $reconcileNode(prevFirstChildKey, dom);\n    } else {\n      const lastDOM = getPrevElementByKeyOrThrow(prevFirstChildKey);\n      const replacementDOM = $createNode(nextFrstChildKey, null, null);\n      try {\n        dom.replaceChild(replacementDOM, lastDOM);\n      } catch (error) {\n        if (typeof error === 'object' && error != null) {\n          const msg = `${error.toString()} Parent: ${\n            dom.tagName\n          }, new child: {tag: ${\n            replacementDOM.tagName\n          } key: ${nextFrstChildKey}}, old child: {tag: ${\n            lastDOM.tagName\n          }, key: ${prevFirstChildKey}}.`;\n          throw new Error(msg);\n        } else {\n          throw error;\n        }\n      }\n      destroyNode(prevFirstChildKey, null);\n    }\n    const nextChildNode = activeNextNodeMap.get(nextFrstChildKey);\n    if ($isTextNode(nextChildNode)) {\n      if (subTreeTextFormat === null) {\n        subTreeTextFormat = nextChildNode.getFormat();\n      }\n      if (subTreeTextStyle === '') {\n        subTreeTextStyle = nextChildNode.getStyle();\n      }\n    }\n  } else {\n    const prevChildren = createChildrenArray(prevElement, activePrevNodeMap);\n    const nextChildren = createChildrenArray(nextElement, activeNextNodeMap);","sourceCodeStart":360,"sourceCodeEnd":396,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/resources/js/wysiwyg/lexical/core/LexicalReconciler.ts#L360-L396","documentation":"Lexical's development-mode reconciler ($reconcileChildren) throws this internal invariant when it must destroy the first child DOM node during reconciliation and detects a tag/key mismatch between the existing DOM child and the replacement node — i.e. Lexical's node state and the rendered DOM have diverged. The message embeds the parent tag, new child tag/key, and old child tag/key to help report the invariant violation upstream.","triggerScenarios":"$reconcileChildrenWithDirection encountering a corrupted reconciliation path during $commitPendingUpdates: typically after a plugin mutates nodes outside updateEditor, or a node transformer/listener mutates the tree during reconciliation.","commonSituations":"Third-party Wysiwyg plugins calling editor APIs outside editor.update(); concurrent mutations from event listeners; Lexical version upgrades with custom plugins relying on old behavior; programmatic node replacement without $applyNodeReplacement.","solutions":["Wrap all node mutations in editor.update() callbacks; never mutate nodes directly from listeners","Use $applyNodeReplacement / proper node APIs when replacing nodes in custom plugins","Reproduce in dev build and report to Lexical maintainers with the tag/key info from the message — this is an internal invariant","Pin/upgrade Lexical and plugins to matching versions"],"exampleFix":"// before\nnode.replace(newNode); // outside editor.update()\n// after\neditor.update(() => {\n  newNode.replace(node);\n});","handlingStrategy":"try-catch","validationCode":"// Ensure all mutations happen inside editor.update()\nif (!editor._updateInProgress /* or via wrapper */) {\n  // refactor: schedule inside update instead of mutating now\n  editor.update(() => performMutation());\n}","typeGuard":"function isInsideLexicalUpdate(fn: () => void): () => void {\n  return () => editor.update(fn);\n}","tryCatchPattern":"try {\n  editor.update(() => applyNodeChanges());\n} catch (e) {\n  console.error('Lexical reconciliation invariant violated', e);\n  // recover: reload editor state from last serialized snapshot\n  editor.setEditorState(lastGoodState);\n}","preventionTips":["Wrap every node mutation in editor.update()","Use $applyNodeReplacement for custom node swaps","Keep Lexical and plugins on matching versions","Test plugins against dev builds to catch invariants early"],"tags":["javascript","lexical","rich-text","reconciler","dev-only"],"backgroundTag":"lexical-reconciler-mismatch","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}