clockworklabs/SpacetimeDB · error · Error
Subscription has already ended
Error message
Subscription has already ended
What it means
unsubscribeThen(onEnd) is only valid while the subscription has not ended. The #endedState flag becomes true when the 'end' event fires (UnsubscribeApplied received, or the subscription otherwise ended). Calling unsubscribeThen after that is a lifecycle violation and throws.
Source
Thrown at crates/bindings-typescript/src/sdk/subscription_builder_impl.ts:247
}
);
}
/**
* Unsubscribes and also registers a callback to run upon success.
* I.e. when an `UnsubscribeApplied` message is received.
*
* If `Unsubscribe` returns an error,
* or if the `on_error` callback(s) are invoked before this subscription would end normally,
* the `on_end` callback is not invoked.
*
* @param onEnd - Callback to run upon successful unsubscribe.
*/
unsubscribeThen(
onEnd: (ctx: SubscriptionEventContextInterface<RemoteModule>) => void
): void {
if (this.#endedState) {
throw new Error('Subscription has already ended');
}
if (this.#unsubscribeCalled) {
throw new Error('Unsubscribe has already been called');
}
this.#unsubscribeCalled = true;
this.db.unregisterSubscription(this.#querySetId);
this.#emitter.on(
'end',
(ctx: SubscriptionEventContextInterface<RemoteModule>) => {
this.#endedState = true;
this.#activeState = false;
onEnd(ctx);
}
);
}
/**
* True if this `SubscriptionHandle` has ended,View on GitHub (pinned to 524b4487d9)
Solutions
- Check handle.isEnded() before calling unsubscribeThen
- Register on_end via the builder/handle's event API instead of triggering teardown on an already-ended subscription
- Clear pending async teardown callbacks (timers, debounces) when the subscription ends
Example fix
// before
setTimeout(() => handle.unsubscribeThen(onEnd), 5000); // may run after 'end' -> throws
// after
setTimeout(() => {
if (!handle.isEnded()) handle.unsubscribeThen(onEnd);
}, 5000); Defensive patterns
Strategy: validation
Validate before calling
if (!handle.isEnded()) {
handle.unsubscribeThen(onEnd);
} Type guard
function canUnsubscribeThen(h: { isEnded(): boolean; isActive(): boolean }): boolean {
return !h.isEnded() && h.isActive();
} Prevention
- Check isEnded() before any deferred unsubscribeThen (timers, debounced unmount)
- Cancel pending teardown callbacks when the subscription's end event fires
When it happens
Trigger: Calling unsubscribeThen(cb) after the 'end' event already fired; calling it after a previous unsubscribe completed and applied; a late async callback (debounced unmount, delayed navigation) racing the server's UnsubscribeApplied.
Common situations: Component teardown racing an already-ended subscription; retry/reconnect logic calling unsubscribeThen on a stale handle after the connection dropped the subscription; user clicking 'unsubscribe' twice with the second handler firing after end.
Related errors
- Unsubscribe has already been called
- Subscriptions must have at least one query
- Subscriptions must be SQL strings or typed queries
- cannot serialize refs without a typespace
- cannot deserialize refs without a typespace
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/572d55eea11f336f.
Report an issue: GitHub.