redis/node-redis · error · Error
TokenManager is not running, but refresh was called
Error message
TokenManager is not running, but refresh was called
What it means
TokenManager.refresh() is the private method invoked by the scheduled refresh timeout. It asserts `this.listener` is non-null because refresh is only meaningful while the manager is running. The guard fires if a refresh fires after stop()/dispose() — i.e. a race where a pending setTimeout callback invokes refresh() after the listener was cleared. The error indicates the lifecycle bookkeeping allowed a late refresh to slip through.
Source
Thrown at packages/client/lib/authx/token-manager.ts:216
if (this.retryAttempt >= maxAttempts) {
return false;
}
if (isRetryable) {
return isRetryable(error, this.retryAttempt);
}
return false;
}
public isRunning(): boolean {
return this.listener !== null;
}
private async refresh(): Promise<void> {
if (!this.listener) {
throw new Error('TokenManager is not running, but refresh was called');
}
try {
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();
}
}
}View on GitHub (pinned to bb5beb5657)
Solutions
- Ensure you only dispose the TokenManager after outstanding token requests settle, or tolerate the resulting rejection.
- Avoid calling start() and dispose() concurrently from different async contexts; serialize lifecycle transitions.
- If you see this outside tests, treat it as a lifecycle bug — confirm stop()/clearTimeout is reached on every dispose path.
- Upgrade the client; if reproducible, report it with the exact start/dispose interleaving.
Defensive patterns
Strategy: try-catch
Validate before calling
if (!tokenManager.isRunning()) {
// do not trigger a refresh path; the manager is stopped
} Try / catch
try {
// ... code that may race with dispose
} catch (err) {
if (err instanceof Error && /not running, but refresh was called/.test(err.message)) {
// benign teardown race; ignore
} else throw err;
} Prevention
- Do not dispose the TokenManager while a refresh is in flight; drain or await it first.
- Serialize start/dispose transitions from a single owner.
- In tests, await outstanding operations before teardown.
When it happens
Trigger: Calling dispose() on the Disposable returned by start() while a refresh is already in-flight or scheduled, and the timeout fires before/after the listener is nulled; a bug in scheduleNextRefresh not canceling the prior timeout on stop (the code does clearTimeout, so hitting this normally implies an unexpected code path or a manual test invocation of refresh).
Common situations: Aggressive teardown in tests or on process shutdown disposing the manager while a token request is mid-flight; double-dispose; a fork/worker race where two actors manage the same TokenManager; direct unit-test invocation of the private refresh path after stop.
Related errors
- TokenManager is not running, but a new token was received
- 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/b85993fb596a6e0a.json.
Report an issue: GitHub.