clockworklabs/SpacetimeDB · error · Error
Subscription has already ended
Error message
Subscription has already ended
What it means
unsubscribeThen registers an on-end callback and issues the unsubscribe; it first refuses to run when the subscription has already ended (#endedState becomes true when the end event fired, e.g. after UnsubscribeApplied was received or an error ended the subscription).
Solutions
- Check handle.isEnded() before calling unsubscribeThen — it reflects the ended flag exactly
- Route all unsubscribe attempts for a handle through one owner so at most one teardown runs
- Track subscription lifecycle in your own state machine when multiple callers may unsubscribe
Example fix
// before
handle.unsubscribeThen(onEnd); // throws if already ended
// after
if (!handle.isEnded()) {
handle.unsubscribeThen(onEnd);
} Defensive patterns
Strategy: validation
Validate before calling
if (!handle.isEnded()) {
handle.unsubscribeThen(onEnd);
} Prevention
- Check isEnded() before unsubscribeThen
- Funnel all teardown through a single owner per handle
- Register your own onEnd/onError callbacks and use them to track lifecycle state
When it happens
Trigger: Calling unsubscribeThen(cb) after the subscription already ended: a previous unsubscribe completed, or on_error ended the subscription, and a second teardown path calls unsubscribeThen on the same handle.
Common situations: Two teardown paths (component unmount and logout flow) both trying to end the same subscription; delayed async code calling unsubscribeThen after the end event already arrived.
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
- Unsubscribe has already been called
- `fromCounterV7` uuid `counter` must be non-negative
- Subscriptions must be SQL strings or typed queries
- Subscriptions must have at least one query
- Brotli compression is not supported by the runtime. Please…
AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20).
Data as JSON: /api/errors/572d55eea11f336f.
Report an issue: GitHub.
Appendix: 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 6dee26c6ef)