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

  1. Check handle.isEnded() before calling unsubscribeThen
  2. Register on_end via the builder/handle's event API instead of triggering teardown on an already-ended subscription
  3. 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

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


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/572d55eea11f336f. Report an issue: GitHub.