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

  1. Verify the protyle is still active before calling trackRange (remove it from your tracking on destroy)
  2. Clear listeners/timers/callbacks holding the protyle when it is destroyed
  3. Catch the error and skip range tracking for the dead instance
  4. 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

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


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