ppy/osu · warning · InvalidOperationException

Cannot toggle ready when in {localUser.State}

Error message

Cannot toggle ready when in {localUser.State}

What it means

Thrown by MultiplayerClient.ToggleReady when the local user's state is not Idle or Ready. Those are the only states from which a ready toggle is valid; any other state (Spectating, Loaded, Playing, Results) falls through to the default case.

Source

Thrown at osu.Game/Online/Multiplayer/MultiplayerClient.cs:447

        public async Task ToggleReady()
        {
            var localUser = LocalUser;

            if (localUser == null)
                return;

            switch (localUser.State)
            {
                case MultiplayerUserState.Idle:
                    await ChangeState(MultiplayerUserState.Ready).ConfigureAwait(false);
                    return;

                case MultiplayerUserState.Ready:
                    await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);
                    return;

                default:
                    throw new InvalidOperationException($"Cannot toggle ready when in {localUser.State}");
            }
        }

        /// <summary>
        /// Toggles the <see cref="LocalUser"/>'s spectating state.
        /// </summary>
        /// <exception cref="InvalidOperationException">If a toggle of the spectating state is not valid at this time.</exception>
        public async Task ToggleSpectate()
        {
            var localUser = LocalUser;

            if (localUser == null)
                return;

            switch (localUser.State)
            {
                case MultiplayerUserState.Idle:
                case MultiplayerUserState.Ready:

View on GitHub (pinned to d9c73e12ad)

Solutions

  1. Only expose the ready toggle when localUser.State is Idle or Ready.
  2. Bind the button's Enabled state to a check on MultiplayerClient.LocalUser?.State.
  3. If hit at runtime, ignore the click (the user must wait for the match to end).

Example fix

// before
readyButton.Action = () => client.ToggleReady();

// after
readyButton.Action = () =>
{
    var s = client.LocalUser?.State;
    if (s == MultiplayerUserState.Idle || s == MultiplayerUserState.Ready)
        client.ToggleReady();
};
Defensive patterns

Strategy: validation

Validate before calling

var s = client.LocalUser?.State;
if (s == MultiplayerUserState.Idle || s == MultiplayerUserState.Ready)
    await client.ToggleReady();

Type guard

bool CanToggleReady(MultiplayerUserState? s)
    => s == MultiplayerUserState.Idle || s == MultiplayerUserState.Ready;

Try / catch

try { await client.ToggleReady(); }
catch (InvalidOperationException) { /* invalid state: ignore the click */ }

Prevention

When it happens

Trigger: Calling ToggleReady() while localUser.State is Spectating, Playing, Loaded, or Results. The state machine rejects the transition.

Common situations: Ready button not disabled during a match; spectating user clicks ready; race where the button is clicked as the match starts.

Related errors


AI-assisted analysis of ppy/osu@d9c73e12ad (2026-08-13). Data as JSON: /api/errors/18893d213aa0c6d4. Report an issue: GitHub.