toeverything/AFFiNE · error · Error

No root node found in the tree

Error message

No root node found in the tree

What it means

_rebuildBlockTree picks draftModels[0] as the root; when the flat draft list is empty, nodeMap.get(undefined) is undefined and it throws this plain Error. An empty list means the snapshot contained no convertible blocks: either snapshot.blocks was empty, or every block failed _snapshotToModel transformation — those failures are only console.error'ed ('Error when transforming snapshot to model data') and filtered out, leaving nothing to rebuild.

Source

Thrown at blocksuite/framework/store/src/transformer/transformer.ts:600

      nodeMap.set(draft.id, { draft, snapshot, children: [] });
    });
    const root = nodeMap.get(draftModels[0].draft.id) as DraftBlockTreeNode;

    // Second pass: build the tree structure
    draftModels.forEach(({ draft, parentId, index }) => {
      const node = nodeMap.get(draft.id);
      if (!node) return;

      if (parentId) {
        const parentNode = nodeMap.get(parentId);
        if (parentNode && index !== undefined) {
          parentNode.children[index] = node;
        }
      }
    });

    if (!root) {
      throw new Error('No root node found in the tree');
    }

    return root;
  }

  private async _snapshotToBlock(
    snapshot: BlockSnapshot,
    doc: Store,
    parent?: string,
    index?: number
  ): Promise<BlockModel | null> {
    this._triggerBeforeImportEvent(snapshot, parent, index);

    const flatSnapshots: FlatSnapshot[] = [];
    this._flattenSnapshot(snapshot, flatSnapshots, parent, index);

    const blockTree = await this._convertFlatSnapshots(flatSnapshots);

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Check snapshot.blocks is present and non-empty before calling the import API.
  2. Watch the console for 'Error when transforming snapshot to model data' entries — they name the real per-block failure that emptied the list.
  3. Fix the block schema/props mismatch causing drafts to be dropped.
  4. Reject or skip empty snapshots upstream instead of passing them to the transformer.

Example fix

// before
const doc = await transformer.snapshotToDoc(collection, snapshot);

// after
if (!snapshot.blocks?.length) {
  throw new Error('snapshot has no blocks to import');
}
const doc = await transformer.snapshotToDoc(collection, snapshot);
Defensive patterns

Strategy: validation

Validate before calling

function countBlocks(snapshot: { blocks?: { children?: unknown[] } }): number {
  return 1 + (snapshot.blocks?.children ?? []).reduce(
    (n, c) => n + countBlocks(c as never), 0
  );
}
if (countBlocks(snapshot) === 0) {
  throw new Error('snapshot contains no blocks');
}
await transformer.snapshotToDoc(collection, snapshot);

Type guard

const snapshotHasBlocks = (snapshot: DocSnapshot): boolean =>
  Boolean(snapshot.blocks);

Try / catch

try {
  const doc = await transformer.snapshotToDoc(collection, snapshot);
} catch (e) {
  if (e instanceof Error && e.message === 'No root node found in the tree') {
    // empty snapshot: check console for per-block transform errors, fix schema/props, retry
  } else throw e;
}

Prevention

When it happens

Trigger: Calling snapshot-to-doc with a snapshot whose blocks array is empty; importing a snapshot where every block's props transformation threw, so all drafts were dropped.

Common situations: Hand-built or truncated snapshot JSON; schema/prop mismatches (e.g. a required prop failing to convert) silently discarding all blocks; slices exported from empty selections.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/e73cd2b405f071ee. Report an issue: GitHub.