dotnet/aspnetcore · error · RuntimeException
The HubConnection failed to transition from the
Error message
The HubConnection failed to transition from the '%s' state to the '%s' state because it was actually in the '%s' state.
What it means
changeState(from, to) is a guarded state transition: it only succeeds if the connection is currently exactly in the 'from' state. If the actual state differs (because another thread already moved it, or the caller assumed the wrong starting state), it logs the mismatch and throws a formatted RuntimeException. This protects the state machine from illegal transitions.
Solutions
- Do not call the internal changeState directly; rely on start()/stop() and the built-in reconnect.
- Serialize lifecycle calls - do not call start() and stop() concurrently or issue overlapping reconnects.
- If wrapping reconnect, use a single-threaded executor and an AtomicReference<HubConnectionState> so transitions are owned by one thread.
Example fix
// before - concurrent lifecycle new Thread(() -> hub.start()).start(); new Thread(() -> hub.stop()).start(); // after - serialized lifecycle ScheduledExecutorService es = Executors.newSingleThreadExecutor(); es.submit(() -> hub.start().blockingAwait()); es.submit(() -> hub.stop().blockingAwait());
Defensive patterns
Strategy: try-catch
Try / catch
// Internal state-machine throw; surface as a connection failure and reconnect.
hub.onClosed(ex -> {
if (ex != null && ex.getMessage().contains("failed to transition")) {
log.warn("Illegal state transition; rebuilding connection");
rebuildHubAndStart();
}
}); Prevention
- Never call start() and stop() concurrently; serialize lifecycle on one thread.
- Do not stack custom reconnect logic that races with the client's own transitions.
- Own all state transitions through a single-threaded executor.
When it happens
Trigger: An internal or plugin code path calls changeState(CONNECTING, CONNECTED) when the state is actually DISCONNECTED (e.g. a late callback after stop()); or two reconnect attempts race and both try to transition from CONNECTING.
Common situations: Concurrent reconnect logic; a transport callback (onClose/onOpen) firing after the user called stop(); bugs in custom retry/reconnect wrappers layered on top of the client.
Related errors
- Circuit state is already in progress
- Circuit state is in progress
- Connection is not active.
- HubConnection trying to negotiate when not in the…
- The HubConnection must be in the disconnected state to…
AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11).
Data as JSON: /api/errors/5a7267bac6679f28.
Report an issue: GitHub.
Appendix: source
Thrown at src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HubConnection.java:1717
}
return this.state;
} finally {
this.lock.unlock();
}
}
public HubConnectionState getHubConnectionState() {
return this.hubConnectionState;
}
public void changeState(HubConnectionState from, HubConnectionState to) {
this.lock.lock();
try {
logger.debug("The HubConnection is attempting to transition from the {} state to the {} state.", from, to);
if (this.hubConnectionState != from) {
logger.debug("The HubConnection failed to transition from the {} state to the {} state because it was actually in the {} state.",
from, to, this.hubConnectionState);
throw new RuntimeException(String.format("The HubConnection failed to transition from the '%s' state to the '%s' state because it was actually in the '%s' state.",
from, to, this.hubConnectionState));
}
this.hubConnectionState = to;
} finally {
this.lock.unlock();
}
}
public void changeState(HubConnectionState to) {
this.lock.lock();
try {
logger.debug("The HubConnection is transitioning from the {} state to the {} state.", this.hubConnectionState, to);
this.hubConnectionState = to;
} finally {
this.lock.unlock();
}
}View on GitHub (pinned to 3600ca084e)