{"record":{"id":"899988cc93b0b762","repo":"redis/node-redis","slug":"tokenmanager-is-not-running-but-a-new-token-was-r","errorCode":null,"errorMessage":"TokenManager is not running, but a new token was received","messagePattern":"TokenManager is not running, but a new token was received","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/client/lib/authx/token-manager.ts","lineNumber":238,"sourceCode":"      await this.identityProvider.requestToken().then(this.handleNewToken);\n      this.retryAttempt = 0;\n    } catch (error) {\n\n      if (this.shouldRetry(error)) {\n        this.retryAttempt++;\n        const retryDelay = this.calculateRetryDelay();\n        this.notifyError(`Token refresh failed (attempt ${this.retryAttempt}), retrying in ${retryDelay}ms: ${error}`, true)\n        this.scheduleNextRefresh(retryDelay);\n      } else {\n        this.notifyError(error, false);\n        this.stop();\n      }\n    }\n  }\n\n  private handleNewToken = async ({ token: nativeToken, ttlMs }: TokenResponse<T>): Promise<void> => {\n    if (!this.listener) {\n      throw new Error('TokenManager is not running, but a new token was received');\n    }\n    const token = this.wrapAndSetCurrentToken(nativeToken, ttlMs);\n    this.listener.onNext(token);\n\n    this.scheduleNextRefresh(this.calculateRefreshTime(token));\n  }\n\n  /**\n   * Creates a Token object from a native token and sets it as the current token.\n   *\n   * @param nativeToken - The raw token received from the identity provider\n   * @param ttlMs - Time-to-live in milliseconds for the token\n   *\n   * @returns A new Token instance containing the wrapped native token and expiration details\n   *\n   */\n  public wrapAndSetCurrentToken(nativeToken: T, ttlMs: number): Token<T> {\n    const now = Date.now();","sourceCodeStart":220,"sourceCodeEnd":256,"githubUrl":"https://github.com/redis/node-redis/blob/90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58/packages/client/lib/authx/token-manager.ts#L220-L256","documentation":"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.","triggerScenarios":"Calling stop() while requestToken() is still pending; the pending promise later resolves and tries to deliver a token to a manager with no listener.","commonSituations":"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.","solutions":["Drain or await the in-flight refresh before calling stop().","Treat this error as expected during teardown and suppress it.","Track an in-flight promise in stop() and resolve it before clearing the listener."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"async function gracefulStop(manager) {\n  // ensure any in-flight requestToken resolves/rejects before clearing listener\n  await manager.drainInFlight?.(); // if available\n  manager.stop();\n}\n\ntry {\n  await manager.refresh();\n} catch (err) {\n  if (/not running/.test(err.message)) {\n    // expected during teardown\n    return;\n  }\n  throw err;\n}","preventionTips":["Drain in-flight token requests before stopping the manager.","Suppress 'not running' errors during documented teardown paths.","Avoid stopping and immediately restarting the same manager; create a new one."],"tags":["lifecycle","race","token-manager","auth","teardown"],"backgroundTag":null,"analyzedSha":"90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58","analyzedAt":"2026-08-11T15:37:21.243Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}