siyuan-note/siyuan · error

cannot stage an empty tree

Error message

cannot stage an empty tree

What it means

Before writing a converted document tree into the temporary staging directory, writeObsidianTempTree rejects a nil tree or a tree without a Root node. Such a tree could not be serialized into a valid .sy document, so staging fails fast with this error.

Source

Thrown at kernel/model/import_obsidian.go:2339

		node.SetIALAttr("updated", util.TimeFromID(node.ID))
		return ast.WalkContinue
	})
	tree.ID = doc.ID
	tree.Box = ""
	tree.Path = doc.TargetPath
	tree.HPath = doc.HPath
	tree.Root.Box = ""
	tree.Root.Path = doc.TargetPath
	tree.Root.Spec = treenode.CurrentSpec
	tree.Root.SetIALAttr("id", doc.ID)
	tree.Root.SetIALAttr("title", doc.Title)
	tree.Root.SetIALAttr("updated", util.TimeFromID(doc.ID))
	tree.Root.RemoveIALAttrsByPrefix("custom-")
}

func writeObsidianTempTree(docsTemp string, tree *parse.Tree) error {
	if tree == nil || tree.Root == nil {
		return errors.New("cannot stage an empty tree")
	}
	if tree.Root.FirstChild == nil {
		tree.Root.AppendChild(treenode.NewParagraph(""))
	}
	treenode.UpgradeSpec(tree)
	tree.Root.SetIALAttr("type", "doc")
	luteEngine := util.NewLute()
	renderer := render.NewJSONRenderer(tree, luteEngine.RenderOptions, luteEngine.ParseOptions)
	data := renderer.Render()
	if !json.Valid(data) {
		return fmt.Errorf("generated tree [%s] is not valid JSON", tree.HPath)
	}
	if !util.UseSingleLineSave {
		var buffer bytes.Buffer
		if err := json.Indent(&buffer, data, "", "\t"); err != nil {
			return err
		}
		data = buffer.Bytes()

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Fix the upstream transform so a nil tree surfaces as a proper parse error (error 347) instead of being passed to writeObsidianTempTree
  2. Check why the tree for that document has no Root — usually a parse.Parse nil result that was swallowed
  3. Re-run the import after fixing the problematic source note
  4. Add an explicit nil-tree check in the caller to report the document name before staging

Example fix

// before: nil tree silently passed on
tree, stats := transformObsidianMarkdown(vault, doc, data)
err := writeObsidianTempTree(docsTemp, tree)
// after: fail fast with document context
if tree == nil || tree.Root == nil {
	return fmt.Errorf("transform produced no tree for doc [%s]", doc.ID)
}
err := writeObsidianTempTree(docsTemp, tree)
Defensive patterns

Strategy: validation

Validate before calling

if tree == nil || tree.Root == nil {
	return fmt.Errorf("transform produced no tree for doc [%s]", doc.ID)
}

Type guard

func hasUsableRoot(t *parse.Tree) bool { return t != nil && t.Root != nil }

Try / catch

if err := writeObsidianTempTree(docsTemp, tree); err != nil && strings.Contains(err.Error(), "empty tree") {
	// find the upstream parse/transform failure that returned a nil tree
}

Prevention

When it happens

Trigger: Calling writeObsidianTempTree(docsTemp, tree) where the transform step previously returned nil (parse failure propagated as a nil *parse.Tree) or produced a Tree whose Root field is nil.

Common situations: A parse failure earlier in the pipeline was ignored (nil tree returned without error), leading to this downstream error instead of the original parse failure; a code path constructing a Tree literal forgot to assign Root.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/21a9c7e085614346. Report an issue: GitHub.