clockworklabs/SpacetimeDB · error · InvalidOperationException

Invalid tag value, this state should be unreachable.

Error message

Invalid tag value, this state should be unreachable.

What it means

For tagged/variant types, the BSATN C# generator emits a read function shaped as `reader.ReadByte() switch { 0 => new VariantA(...), 1 => new VariantB(...), _ => throw new InvalidOperationException("Invalid tag value, this state should be unreachable.") }`. The fallback fires when the byte stream contains a variant tag with no generated case — in practice this means the payload was encoded by a module whose variant layout differs from the client's generated bindings, or the buffer is truncated/misaligned.

Solutions

  1. Regenerate the C# bindings from the exact module version the client connects to and redeploy
  2. Treat variant/enum layout changes as breaking changes: migrate clients before servers, or publish under a new database name
  3. If hand-decoding, verify tag values and byte offsets against the module schema
  4. When versions are confirmed in sync, log raw bytes on failure to check for truncated payloads

Example fix

# before: server reorders a tagged enum's variants; client keeps old generated reader
# tag byte 2 no longer matches any case -> InvalidOperationException

# after: regenerate from the deployed module and rebuild the client
$ spacetime generate --lang csharp --out-dir ./generated --project-path ./server
$ dotnet build
Defensive patterns

Strategy: try-catch

Validate before calling

// Round-trip test against server-encoded fixtures in CI:
// var bytes = LoadFixture("player_event.bsatn");
// ReducerEventArgs.Read(new BinaryReader(new MemoryStream(bytes))); // must not throw
// Regenerate fixtures whenever the module schema changes.

Try / catch

try {
    var evt = ReadEvent(reader);
} catch (InvalidOperationException e) when (e.Message.Contains("Invalid tag value")) {
    // stale bindings or corrupt frame: log, reconnect, and prompt the user to update
    Logger.LogError("BSATN tag mismatch — regenerate client bindings");
}

Prevention

When it happens

Trigger: Deserializing a BSATN payload whose tag byte does not match any generated case: variants added, removed, or reordered server-side while the client runs older generated code; manually crafted BSATN fixtures with wrong tag values; truncated or offset-corrupted byte buffers.

Common situations: Module republished with changed enum/variant layout while clients keep stale assemblies; test harnesses hand-encoding BSATN; partial writes from a flaky websocket being fed to the parser.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/5dd8639277d5e360. Report an issue: GitHub.

Appendix: source

Thrown at crates/bindings-csharp/BSATN.Codegen/Type.cs:654

                            {
                                public override string ToString() =>
                                    $"{{m.Name}}({ SpacetimeDB.BSATN.StringUtil.GenericToString({{m.Identifier}}_) })";
                            }
                        
                        """
                    )
                )
            );

            read = $$"""
                    return reader.ReadByte() switch {
                        {{string.Join(
                            "\n            ",
                            bsatnDecls.Select((m, i) =>
                                $"{i} => new {m.Identifier}({m.Identifier}{TypeUse.BsatnFieldSuffix}.Read(reader)),"
                            )
                        )}}
                        _ => throw new System.InvalidOperationException("Invalid tag value, this state should be unreachable.")
                    };
            """;

            write = $$"""
            switch (value) {
            {{string.Join(
                "\n",
                bsatnDecls.Select((m, i) => $"""
                                                            case {m.Identifier}(var inner):
                                                                writer.Write((byte){i});
                                                                {m.Identifier}{TypeUse.BsatnFieldSuffix}.Write(writer, inner);
                                                                break;
                                                """))}}
                        }
            """;

            getHashCode = $$"""
            switch (this) {

View on GitHub (pinned to 6dee26c6ef)