redis/node-redis · error · 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 async continuation attached to identityProvider.requestToken().then(...). It asserts the listener is still present when the token arrives. If the manager was stopped/disposed between issuing the request and receiving the token, the listener is null and the newly arrived token has nowhere to go — so it throws rather than silently scheduling a refresh against a dead manager.
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 bb5beb5657)
Solutions
- Await or otherwise drain in-flight token requests before calling dispose() (track the outstanding promise and await it).
- Tolerate the rejection in your error handler — it indicates a benign teardown race, not a token problem.
- Avoid start/dispose ping-ponging; keep the TokenManager alive for the process lifetime.
- If reproducible deterministically, report it as a lifecycle bug with the interleaving.
Defensive patterns
Strategy: try-catch
Try / catch
try {
// operation during which dispose() may run
} catch (err) {
if (err instanceof Error && /not running, but a new token was received/.test(err.message)) {
// token arrived after dispose; safe to ignore
} else throw err;
} Prevention
- Track and await in-flight requestToken() promises before disposing.
- Keep the TokenManager alive for the process lifetime rather than churning start/stop.
When it happens
Trigger: Calling dispose() while a requestToken() promise is pending; the token resolves after stop() has nulled the listener. Internal race: the refresh timeout callback starts a request, and dispose runs before the request settles.
Common situations: Short-lived processes disposing the token manager before the IDP responds; tests that tear down the manager without awaiting in-flight requests; an IDP with high latency combined with rapid start/stop cycling.
Related errors
- TokenManager is not running, but refresh was called
- TokenManager is not running but received an error: ${errorMe
- expirationRefreshRatio must be less than or equal to 1
- expirationRefreshRatio must be greater or equal to 0
- The client is closed
AI-assisted analysis of redis/node-redis@bb5beb5657 (2026-08-03).
Data as JSON: /data/errors/899988cc93b0b762.json.
Report an issue: GitHub.