siyuan-note/siyuan · warning

block updates are empty

Error message

block updates are empty

What it means

buildBlockUpdateOperations (the engine behind BuildBlockUpdateOperations) rejects the request up front when the inputs array is empty, returning "block updates are empty". Batch block updates require at least one BlockUpdateInput; there is nothing to validate or transact otherwise.

Solutions

  1. Check inputs.length (or operations.length) > 0 before invoking the update API and skip the call when empty.
  2. Ensure your local filter/validation isn't stripping all entries before the call.
  3. Guard the flush call site with a flag that tracks whether any update was actually queued.

Example fix

// before
await fetchPost('/api/block/updateBlock', { operations: pending });
// after
if (pending.length > 0) {
  await fetchPost('/api/block/updateBlock', { operations: pending });
}
Defensive patterns

Strategy: validation

Validate before calling

function assertNonEmpty(updates) {
  if (!Array.isArray(updates) || updates.length === 0) {
    throw new SkipUpdate('nothing to update');
  }
}

Prevention

When it happens

Trigger: Calling the block update API with an empty operations/inputs list — e.g. a caller that filtered out all pending updates locally, or passed an uninitialized array.

Common situations: Plugins that accumulate edits into a batch and flush only when something changed, but flush unconditionally; clients serializing an empty selection or clipboard state.

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@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/c6d72ecc0d9d4003. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/block_update.go:85

	for _, queued := range takeQueuedTransactions() {
		flushTx(queued)
	}

	operations, rootIDs, err := build(inputs)
	if err != nil {
		return nil, nil, err
	}

	transaction := &Transaction{DoOperations: operations}
	if err = performTxSyncLocked(transaction); err != nil {
		return nil, nil, err
	}
	return []*Transaction{transaction}, rootIDs, nil
}

func buildBlockUpdateOperations(inputs []BlockUpdateInput, resolveTree blockUpdateTreeResolver, loadTree blockUpdateTreeLoader) (operations []*Operation, rootIDs []string, err error) {
	if 1 > len(inputs) {
		return nil, nil, errors.New("block updates are empty")
	}

	luteEngine := util.NewLute()
	rootIDSet := map[string]struct{}{}
	treeCache := map[blockUpdateTreeKey]*parse.Tree{}
	for _, input := range inputs {
		if !ast.IsNodeIDPattern(input.ID) {
			return nil, nil, fmt.Errorf("invalid block ID [%s]", input.ID)
		}

		data, dataTree, parseErr := parseBlockUpdateData(input.Data, input.DataType, luteEngine)
		if parseErr != nil {
			return nil, nil, parseErr
		}

		var oldTree *parse.Tree
		var cacheKey blockUpdateTreeKey
		hasCacheKey := false

View on GitHub (pinned to 9f775e8a12)