clockworklabs/SpacetimeDB · error · Error
Unsubscribe has already been called
Error message
Unsubscribe has already been called
What it means
SubscriptionHandleImpl.unsubscribe consumes the handle: the first call unregisters the query set and schedules the Unsubscribe message; the private #unsubscribeCalled flag makes any second call throw. This is use-once semantics - the handle cannot be unsubscribed twice.
Source
Thrown at crates/bindings-typescript/src/sdk/subscription_builder_impl.ts:220
onError(ctx, error);
}
}
);
this.#querySetId = this.db.registerSubscription(
this,
this.#emitter,
querySql
);
}
/**
* Consumes self and issues an `Unsubscribe` message,
* removing this query from the client's set of subscribed queries.
* It is only valid to call this method if `is_active()` is `true`.
*/
unsubscribe(): void {
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;
}
);
}
/**
* 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,View on GitHub (pinned to 524b4487d9)
Solutions
- Track whether you already unsubscribed (a boolean next to the handle) and guard the call
- In shared-handle setups, unsubscribe from exactly one owner (or reference-count it yourself)
- In React StrictMode, keep the handle in a ref and make cleanup idempotent with your own flag
Example fix
// before
useEffect(() => {
const h = builder.subscribe(qs);
return () => h.unsubscribe(); // called twice under StrictMode -> throws
}, []);
// after
const unsubRef = useRef(false);
useEffect(() => {
const h = builder.subscribe(qs);
return () => {
if (unsubRef.current) return;
unsubRef.current = true;
h.unsubscribe();
};
}, []); Defensive patterns
Strategy: validation
Validate before calling
let unsubscribed = false;
const safeUnsubscribe = () => {
if (unsubscribed || handle.isEnded()) return;
unsubscribed = true;
handle.unsubscribe();
}; Try / catch
try {
handle.unsubscribe();
} catch (e) {
if (e instanceof Error && e.message === 'Unsubscribe has already been called') {
// benign double-teardown (e.g. StrictMode): ignore
} else throw e;
} Prevention
- Wrap handle teardown in one idempotent helper and call only that helper
- In React StrictMode, guard effect cleanup with a ref flag
- Give each subscription exactly one owner responsible for unsubscribing
When it happens
Trigger: Calling unsubscribe() twice on the same handle; calling unsubscribe() after already calling unsubscribeThen() on the same handle; React StrictMode double-invoking an effect cleanup that calls unsubscribe.
Common situations: Dev-mode double effect cleanup (StrictMode runs mount/unmount twice); error-handling paths that unsubscribe again after a teardown path already did; multiple components sharing one handle and each unsubscribing on unmount.
Related errors
- Subscription has already ended
- 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/8b54441c5c125d77.
Report an issue: GitHub.