siyuan-note/siyuan · error
Cannot track a range in a destroyed Protyle instance
Error message
Cannot track a range in a destroyed Protyle instance
What it means
Thrown by trackRange when the passed IProtyle instance has been destroyed (it is in the destroyedProtyles set). Range tracking relies on live editor internals, so tracking in a destroyed editor is refused up front. Callers must only track ranges on active Protyle instances.
Source
Thrown at app/src/protyle/util/trackedRange.ts:1134
removeInputListeners(protyle);
}
if (state.owner) {
const handles = ownedHandles.get(state.owner);
handles?.forEach(item => {
if (item.protyle === protyle && item.handle === handle) {
handles.delete(item);
}
});
if (handles?.size === 0) {
ownedHandles.delete(state.owner);
}
}
};
export const trackRange = (protyle: IProtyle, range: Range,
options: ITrackRangeOptions): ITrackedRangeHandle => {
if (destroyedProtyles.has(protyle)) {
throw new Error("Cannot track a range in a destroyed Protyle instance");
}
if (!options?.owner || !["function", "object"].includes(typeof options.owner)) {
throw new TypeError("The tracked range owner is required");
}
if (options.affinity && !["after", "before"].includes(options.affinity)) {
throw new TypeError("The tracked range affinity must be before or after");
}
if (unloadingPlugins.has(options.owner)) {
throw new Error("Cannot track a range for an unloaded plugin");
}
const rangeSnapshot = getRangeSnapshot(protyle, range);
if (!rangeSnapshot) {
throw new TypeError("The range must be inside one editable source block of this Protyle instance");
}
const targetTokens = rangeSnapshot.stream.tokens.slice(rangeSnapshot.start, rangeSnapshot.end);
if (!range.collapsed && targetTokens.length === 0) {
throw new TypeError("The tracked range must contain semantic content");
}View on GitHub (pinned to 8641553a1f)
Solutions
- Verify the protyle is still active before calling trackRange (remove it from your tracking on destroy)
- Clear listeners/timers/callbacks holding the protyle when it is destroyed
- Catch the error and skip range tracking for the dead instance
- Re-acquire the current protyle from the layout (e.g. the active editor) instead of a cached reference
Example fix
// before
trackRange(cachedProtyle, range, {owner: plugin});
// after
if (isProtyleDestroyed(cachedProtyle)) return;
trackRange(cachedProtyle, range, {owner: plugin}); Defensive patterns
Strategy: try-catch
Try / catch
try {
trackRange(protyle, range, {owner: plugin});
} catch (e) {
if (e.message.startsWith("Cannot track a range in a destroyed")) return; // editor already closed
throw e;
} Prevention
- Unhook all async callbacks and listeners when a Protyle is destroyed
- Never cache protyle references across tab closes; fetch the active editor each time
- Check whether the editor is still open before range operations in deferred code
- Centralize editor access so destroy can proactively cancel pending range work
When it happens
Trigger: Calling trackRange(protyle, range, options) after the protyle was destroyed (e.g. after protyle.destroy() or a tab/editor close) — typically from a stale callback or a plugin holding a stale protyle reference.
Common situations: Plugins caching protyle across tab switches; asynchronous callbacks (fetch, timers) resolving after the editor was destroyed; event listeners not unregistered on destroy.
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
- Recorder has been disposed
- The tracked range owner is required
- The tracked range affinity must be before or after
- View state service has been destroyed
- MCP request operation scope is closed
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/54e90bf8c70008cc.
Report an issue: GitHub.