facebook/lexical · error

$createTextNodesFromYText: Node ${type} is not a TextNode

Error message

$createTextNodesFromYText: Node ${type} is not a TextNode

What it means

SyncV2 maps Y.Text regions to Lexical TextNodes. After instantiating the class registered for a type, it asserts the result is actually a TextNode ($isTextNode). A registered class with a matching type string but a non-text base class (ElementNode, DecoratorNode, etc.) cannot hold inline Y.Text content, so the sync throws.

Source

Thrown at packages/lexical-yjs/src/SyncV2.ts:404

  const nodeTypes: string[] = deltas.map(
    delta => delta.attributes.t ?? TextNode.getType(),
  );
  const canReuseNodes =
    nodes.length === nodeTypes.length &&
    nodes.every((node, i) => node.getType() === nodeTypes[i]);
  if (!canReuseNodes) {
    const registeredNodes = binding.editor._nodes;
    nodes = nodeTypes.map(type => {
      const nodeInfo = registeredNodes.get(type);
      if (nodeInfo === undefined) {
        throw new Error(
          `$createTextNodesFromYText: Node ${type} is not registered`,
        );
      }
      const node = new nodeInfo.klass();
      if (!$isTextNode(node)) {
        throw new Error(
          `$createTextNodesFromYText: Node ${type} is not a TextNode`,
        );
      }
      return node;
    });
  }

  // Sync text, properties and state to the text nodes.
  for (let i = 0; i < deltas.length; i++) {
    const node = nodes[i];
    const delta = deltas[i];
    const {attributes, insert} = delta;
    if (node.__text !== insert) {
      node.setTextContent(insert);
    }
    const properties = {
      ...getDefaultNodeProperties(node, binding),
      ...attributes.p,

View on GitHub (pinned to 76a22dcba9)

Solutions

  1. Make the registered class extend TextNode (or a TextNode subclass) so $isTextNode passes.
  2. If the node is meant to be an element/decorator, remove its type from the Y.Text node-type data and remap the document content.
  3. Check the class's $config/extends to confirm the inheritance chain: class MyNode extends TextNode.
  4. Ensure serialized Yjs data was not produced by a version where the node was still a TextNode.

Example fix

// before
class MentionNode extends ElementNode { getType() { return 'mention'; } }
// after
class MentionNode extends TextNode { getType() { return 'mention'; } }
Defensive patterns

Strategy: type-guard

Validate before calling

const nodeInfo = editor._nodes.get(type);
if (!nodeInfo) throw new Error(`Type ${type} not registered`);
const probe = new nodeInfo.klass();
if (!$isTextNode(probe)) throw new Error(`${type} is not a TextNode`);

Type guard

function isTextSubclass(klass: Klass<LexicalNode>): boolean {
  return klass.prototype instanceof TextNode || klass === TextNode;
}

Try / catch

try {
  syncYTextToLexical(binding, yText);
} catch (e) {
  if (e instanceof Error && e.message.includes('is not a TextNode')) {
    console.error(`Node type registered with wrong base class: ${e.message}`);
  } else { throw e; }
}

Prevention

When it happens

Trigger: nodeTypes map yields a type whose registered klass is not a TextNode subclass when `new nodeInfo.klass()` runs inside $createOrUpdateTextNodesFromYText — e.g. a custom node extending ElementNode but declared in the Y.Text's node-type list.

Common situations: Custom node registered with the same type string as a text-level node but implemented from the wrong base class; refactoring a TextNode subclass to ElementNode while old Yjs docs still reference the old type; copy-pasting a node class with the wrong extends clause.

Related errors


AI-assisted analysis of facebook/lexical@76a22dcba9 (2026-08-31). Data as JSON: /api/errors/0cf60d3ee47cd08a. Report an issue: GitHub.