siyuan-note/siyuan · error

View state service has been destroyed

Error message

View state service has been destroyed

What it means

ViewStateService.ensureActive throws this when set() or patch() is called after the service has been destroyed (destroy() was invoked and its flush completed). The service intentionally refuses new mutations because its pending buffers and timer are torn down and writes would be silently lost.

Source

Thrown at app/src/util/viewState.ts:260

    private scheduleFlush() {
        this.clearFlushTimer();
        this.flushTimer = setTimeout(() => {
            this.flushTimer = undefined;
            this.flush().catch((error) => console.error(error));
        }, this.flushDelay);
    }

    private clearFlushTimer() {
        if (this.flushTimer !== undefined) {
            clearTimeout(this.flushTimer);
            this.flushTimer = undefined;
        }
    }

    private ensureActive() {
        if (this.destroyed) {
            throw new Error("View state service has been destroyed");
        }
    }
}

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Stop using the service after destroy(): guard call sites with a check that the owning view is still open
  2. Unbind event listeners and cancel timers/promises that reference the service in the destroy path
  3. If the code may legitimately outlive one instance, create a fresh ViewStateService (same identity) instead of reusing the destroyed one
  4. Ignore or downgrade the thrown error when the write is fire-and-forget best-effort UI state

Example fix

// before
setTimeout(() => service.set("scroll", pos), 500); // service may be destroyed by then
// after
const timer = setTimeout(() => {
    if (!isViewClosed) service.set("scroll", pos);
}, 500);
onDestroy(() => clearTimeout(timer));
Defensive patterns

Strategy: try-catch

Validate before calling

let active = false;
try {
    service.set("scroll", pos); // throws if destroyed
    active = true;
} catch {
    // service destroyed; skip the write
}

Type guard

const isUsable = (service: ViewStateService | undefined | null): service is ViewStateService =>
    service instanceof ViewStateService; // pair with an isDestroyed flag maintained by the owner
// owner-side guard:
const canWrite = (service: ViewStateService | undefined) => service !== undefined && !destroyed;

Try / catch

try {
    service.set(field, value);
} catch (error) {
    if (error instanceof Error && error.message === "View state service has been destroyed") {
        return; // expected during teardown; ignore best-effort state writes
    }
    throw error;
}

Prevention

When it happens

Trigger: Holding a reference to a ViewStateService after calling destroy() and then invoking set() or patch(). Typical in editors: the view closes and destroys its service, but an in-flight async callback, event handler, or debounced update still fires against the old instance.

Common situations: Tab/window closed or view re-created while a setTimeout/await continuation still calls set(); an event listener not unbound on view close; a component re-render racing with destroy during layout changes.

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/7bad00447dd7f1c3. Report an issue: GitHub.