ppy/osu · warning · InvalidOperationException

Cannot toggle spectate when in {localUser.State}

Error message

Cannot toggle spectate when in {localUser.State}

What it means

Thrown by MultiplayerClient.ToggleSpectate when the local user's state is not Idle, Ready, or Spectating. The toggle only flips between spectating and idle from those entry points; mid-match states are rejected.

Source

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

        {
            var localUser = LocalUser;

            if (localUser == null)
                return;

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

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

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

        public abstract Task TransferHost(int userId);

        public abstract Task KickUser(int userId);

        public abstract Task ChangeSettings(MultiplayerRoomSettings settings);

        public abstract Task ChangeState(MultiplayerUserState newState);

        public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability);

        public abstract Task ChangeUserStyle(int? beatmapId, int? rulesetId);

        /// <summary>
        /// Change the local user's mods in the currently joined room.
        /// </summary>

View on GitHub (pinned to d9c73e12ad)

Solutions

  1. Gate the spectate toggle on localUser.State being Idle, Ready, or Spectating.
  2. Disable the spectate button while in Loaded/Playing/Results.
  3. If encountered, defer the toggle until the match ends and state returns to Idle.

Example fix

// before
spectateButton.Action = () => client.ToggleSpectate();

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

Strategy: validation

Validate before calling

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

Type guard

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

Try / catch

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

Prevention

When it happens

Trigger: Calling ToggleSpectate() while localUser.State is Loaded, Playing, or Results.

Common situations: Spectate toggle clicked during an active match; UI not disabled while the user is playing.

Related errors


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