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
- Stop using the service after destroy(): guard call sites with a check that the owning view is still open
- Unbind event listeners and cancel timers/promises that reference the service in the destroy path
- If the code may legitimately outlive one instance, create a fresh ViewStateService (same identity) instead of reusing the destroyed one
- 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
- Null out the service reference when the view is destroyed so stale callbacks cannot reach it
- Cancel timers, promises, and event listeners in the destroy path before calling destroy()
- Route all writes through one owner method that checks an isClosed flag
- Never cache the service in module-level variables shared across view lifecycles
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
- View state value is too large
- Unable to create a canvas context for the PDF rectangle anno
- Unable to load English commands: ${response.status}
- Failed to save agent session
- Failed to remove agent session
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/7bad00447dd7f1c3.
Report an issue: GitHub.