dotnet/orleans · error · ApplicationException
Can't join game once its over
Error message
Can't join game once its over
What it means
Thrown by TicTacToe GameGrain.AddPlayerToGame when the game's state is already Finished — you cannot add a player to a completed game. The grain is a state machine (AwaitingPlayers -> InPlay -> Finished) and this guards an illegal transition.
Source
Thrown at samples/TicTacToe/Grains/GameGrain.cs:50
{
// make sure newly formed game is in correct state
_playerIds = new List<Guid>();
_moves = new List<GameMove>();
_indexNextPlayerToMove = -1; // safety default - is set when game begins to 0 or 1
_board = new int[3, 3] { { -1, -1, -1 }, { -1, -1, -1 }, { -1, -1, -1 } }; // -1 is empty
_gameState = GameState.AwaitingPlayers;
_winnerId = Guid.Empty;
_loserId = Guid.Empty;
return base.OnActivateAsync(token);
}
// add a player into a game
public Task<GameState> AddPlayerToGame(Guid player)
{
// check if its ok to join this game
if (_gameState is GameState.Finished) throw new ApplicationException("Can't join game once its over");
if (_gameState is GameState.InPlay) throw new ApplicationException("Can't join game once its in play");
// add player
_playerIds.Add(player);
// check if the game is ready to play
if (_gameState is GameState.AwaitingPlayers && _playerIds.Count is 2)
{
// a new game is starting
_gameState = GameState.InPlay;
_indexNextPlayerToMove = Random.Shared.Next(0, 1); // random as to who has the first move
}
// let user know if game is ready or not
return Task.FromResult(_gameState);
}
// make a move during the gameView on GitHub (pinned to fca799fa70)
Solutions
- Create or join a fresh game (new grain key) instead of reusing a Finished one.
- Reset the grain (the OnActivateAsync path sets AwaitingPlayers) — but only a new activation/deactivation cycle does that, so prefer a new game id.
- Have the client check the returned GameState before attempting to join.
Example fix
// before var state = await game.AddPlayerToGame(playerId); // throws if Finished // after: verify state, or start a new game var state = await game.GetState(); if (state == GameState.Finished) game = GrainFactory.GetGrain<IGameGrain>(Guid.NewGuid()); await game.AddPlayerToGame(playerId);
Defensive patterns
Strategy: validation
Validate before calling
// Check the game state before joining
var state = await game.GetState();
if (state == GameState.Finished || state == GameState.InPlay)
return; // do not call AddPlayerToGame Type guard
null
Try / catch
try { await game.AddPlayerToGame(playerId); }
catch (ApplicationException ex) when (ex.Message.Contains("over"))
{ /* start a new game instead */ } Prevention
- Always start a new game grain id once a game is Finished.
- Check returned GameState before attempting to join.
- Track game lifecycle in the client to avoid joining completed games.
When it happens
Trigger: A client calls AddPlayerToGame(playerId) on a game grain whose _gameState is GameState.Finished (a winner has been decided). Also thrown implicitly via the sibling check for InPlay with a different message.
Common situations: Joining a game id that already completed; a stale UI showing an old game as joinable; reusing a finished game grain id instead of creating a new one.
Related errors
- This game is not in play
- No such playerid for this game
- Bad co-ordinates for a move
- Missing value for {name}.
- Invalid option {optionId}
AI-assisted analysis of dotnet/orleans@fca799fa70 (2026-08-13).
Data as JSON: /api/errors/a9f34705e6f1840f.
Report an issue: GitHub.