siyuan-note/siyuan · error

ErrBoxClosed

ErrBoxClosed

Error message

notebook closed

What it means

ErrBoxClosed is the sentinel error indicating the target notebook exists but is closed; most write and index operations refuse to touch closed notebooks. Callers such as the CLI formatNotebookWriteError translate it into a hint telling the user to run `notebook open --id <boxID>` first.

Solutions

  1. Open the notebook first: POST /api/notebook/openNotebook {"notebook": "<boxID>"} or run `siyuan-kernel notebook open --id <boxID>`
  2. Check notebook state with lsNotebooks before writing and open it if closed
  3. In Go callers, branch on errors.Is(err, model.ErrBoxClosed) to give the user an actionable message
  4. Ensure workspace boot/open sequence completes before scripts that touch the notebook run

Example fix

// before
err := writeDoc(boxID, path, data)
if err != nil { return err }

// after
if err != nil {
    if errors.Is(err, model.ErrBoxClosed) {
        return fmt.Errorf("notebook [%s] is closed; run `notebook open --id %s` first", boxID, boxID)
    }
    return err
}
Defensive patterns

Strategy: validation

Validate before calling

// TypeScript: check notebook state and open if closed before writing
const conf = await fetchPost('/api/notebook/lsNotebooks', {});
const nb = conf.data.notebooks.find(n => n.id === boxID);
if (nb && nb.closed) await fetchPost('/api/notebook/openNotebook', { notebook: boxID });

Type guard

function isBoxClosed(err: unknown): err is Error {
  return err instanceof Error && err.message === 'notebook closed';
}

Try / catch

// Go
if errors.Is(err, model.ErrBoxClosed) {
    return fmt.Errorf("notebook [%s] is closed; run `notebook open --id %s` first", boxID, boxID)
}

Prevention

When it happens

Trigger: Writing to or reading docs of a notebook whose .si/config was closed via /api/notebook/closeNotebook (or the UI); the CLI notebook write/import paths hit it and formatNotebookWriteError rewraps it with the open command; getOpenedBox rejects a closed box.

Common situations: User closed the notebook in the UI while automation/CLI scripts still target it; headless scripts operate on notebooks that default to closed after workspace restore; a plugin runs before the notebook is opened at boot.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/4ea9007d856a5517. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/tree.go:211

			logging.LogErrorf("decrypt tree [path=%s] failed: %s", localPath, err)
			return
		}
	}

	if err = treenode.CheckSpecJSON(data); nil != err {
		return
	}
	ret, err = dataparser.ParseJSONWithoutFix(data, luteEngine.ParseOptions)
	if err != nil {
		logging.LogErrorf("parse json to tree [%s] failed: %s", localPath, err)
		return
	}
	return
}

var (
	ErrBoxNotFound   = errors.New("notebook not found")
	ErrBoxClosed     = errors.New("notebook closed")
	ErrBlockNotFound = errors.New("block not found")
	ErrTreeNotFound  = errors.New("tree not found")
	ErrIndexing      = errors.New("indexing")
	ErrBoxUnindexed  = errors.New("notebook unindexed")
	ErrInvalidID     = errors.New("invalid id")
)

func LoadTreeByBlockIDWithReindex(id string) (ret *parse.Tree, err error) {
	return LoadTreeByBlockIDWithReindexInBox(id, "")
}

// LoadTreeByBlockIDWithReindexInBox 与 LoadTreeByBlockIDWithReindex 一致,但按 boxID 路由 blocktree 查询。
func LoadTreeByBlockIDWithReindexInBox(id, boxID string) (ret *parse.Tree, err error) {
	if "" == id {
		logging.LogWarnf("block id is empty")
		return nil, ErrTreeNotFound
	}

View on GitHub (pinned to 9f775e8a12)