redis/node-redis · warning · Error

TokenManager is not running, but a new token was received

Error message

TokenManager is not running, but a new token was received

What it means

handleNewToken is the .then continuation of identityProvider.requestToken(). If a requestToken() was in flight when stop() ran, its late resolution reaches handleNewToken after the listener is already null, and this guard throws. It is essentially a lifecycle race between teardown and an outstanding token fetch.

Solutions

  1. Drain or await the in-flight refresh before calling stop().
  2. Treat this error as expected during teardown and suppress it.
  3. Track an in-flight promise in stop() and resolve it before clearing the listener.
Defensive patterns

Strategy: try-catch

Try / catch

async function gracefulStop(manager) {
  // ensure any in-flight requestToken resolves/rejects before clearing listener
  await manager.drainInFlight?.(); // if available
  manager.stop();
}

try {
  await manager.refresh();
} catch (err) {
  if (/not running/.test(err.message)) {
    // expected during teardown
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling stop() while requestToken() is still pending; the pending promise later resolves and tries to deliver a token to a manager with no listener.

Common situations: Process shutdown during a slow identity provider; test teardown that does not await in-flight refresh; reconnect logic that stops and immediately restarts the manager.

Related errors


AI-assisted analysis of redis/node-redis@90fd0652bc (2026-08-11). Data as JSON: /api/errors/899988cc93b0b762. Report an issue: GitHub.

Appendix: source

Thrown at packages/client/lib/authx/token-manager.ts:238

      await this.identityProvider.requestToken().then(this.handleNewToken);
      this.retryAttempt = 0;
    } catch (error) {

      if (this.shouldRetry(error)) {
        this.retryAttempt++;
        const retryDelay = this.calculateRetryDelay();
        this.notifyError(`Token refresh failed (attempt ${this.retryAttempt}), retrying in ${retryDelay}ms: ${error}`, true)
        this.scheduleNextRefresh(retryDelay);
      } else {
        this.notifyError(error, false);
        this.stop();
      }
    }
  }

  private handleNewToken = async ({ token: nativeToken, ttlMs }: TokenResponse<T>): Promise<void> => {
    if (!this.listener) {
      throw new Error('TokenManager is not running, but a new token was received');
    }
    const token = this.wrapAndSetCurrentToken(nativeToken, ttlMs);
    this.listener.onNext(token);

    this.scheduleNextRefresh(this.calculateRefreshTime(token));
  }

  /**
   * Creates a Token object from a native token and sets it as the current token.
   *
   * @param nativeToken - The raw token received from the identity provider
   * @param ttlMs - Time-to-live in milliseconds for the token
   *
   * @returns A new Token instance containing the wrapped native token and expiration details
   *
   */
  public wrapAndSetCurrentToken(nativeToken: T, ttlMs: number): Token<T> {
    const now = Date.now();

View on GitHub (pinned to 90fd0652bc)