clockworklabs/SpacetimeDB · error · ArgumentOutOfRangeException

Unknown reducer

Error message

Unknown reducer {reducer}

What it means

The C# code generator emits a switch expression that maps incoming Reducer.{PascalName} event objects to generated Invoke{Reducer} handlers, ending with `_ => throw new ArgumentOutOfRangeException(...)`. At runtime this fires when the client receives a reducer-call event for a reducer that has no case in the generated switch — almost always because the client's generated bindings are older than the module the server is running.

Solutions

  1. Regenerate the C# bindings from the deployed module version (`spacetime generate --lang csharp --out-dir ... --project-path <module>`) and ship a client update
  2. Deploy in lockstep: publish the new module only after clients that understand the new reducers have rolled out, or gate new reducers behind a version check
  3. Verify each client connects to the database version its bindings were generated from
  4. Audit the generated Reducer switch cases against the module's reducer list before release

Example fix

// before: module adds a reducer, client keeps old generated bindings
#[spacereduce]
function mint_token(ctx, amount) { ... }
// old client receives Reducer.MintToken -> ArgumentOutOfRangeException

// after: regenerate and rebuild the client
// $ spacetime generate --lang csharp --out-dir ./generated --project-path ./module
// generated switch now contains:
// Reducer.MintToken args => Reducers.InvokeMintToken(eventContext, args),
Defensive patterns

Strategy: try-catch

Validate before calling

// After generating bindings, assert the generated reducer set matches the module:
// (run in CI against the published module schema)
// spacetime calls list <db> --json | compare with Reducer.* cases in generated code

Try / catch

try {
    await conn.SubscriptionBuilder().OnApplied(...).Subscribe(...);
} catch (ArgumentOutOfRangeException e) when (e.ParamName == "Reducer") {
    // unknown reducer event: bindings are stale -> prompt regeneration/update
    Logger.LogError($"Stale client bindings: {e.Message}");
}

Prevention

When it happens

Trigger: The server module is republished with new (or renamed) reducers while a client still runs bindings generated from the older schema; a long-lived client connects to a database whose module was updated; the ReducerEventArgs payload is constructed/dispatched with a name not present in the generated Reducer class.

Common situations: CI auto-publishes the module but the client app build skips `spacetime generate`; desktop/mobile clients that do not auto-update connecting to a freshly published module; publishing schema changes to a shared database name used by multiple client versions.

Related errors


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

Appendix: source

Thrown at crates/codegen/src/csharp.rs:1246

                output,
                "protected override bool Dispatch(IReducerEventContext context, Reducer reducer)"
            );
            indented_block(output, |output| {
                writeln!(output, "var eventContext = (ReducerEventContext)context;");
                writeln!(output, "return reducer switch {{");
                {
                    indent_scope!(output);
                    for reducer_name in
                        iter_reducers(module, options.visibility).map(|r| r.accessor_name.deref().to_case(Case::Pascal))
                    {
                        writeln!(
                            output,
                            "Reducer.{reducer_name} args => Reducers.Invoke{reducer_name}(eventContext, args),"
                        );
                    }
                    writeln!(
                        output,
                        r#"_ => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {{reducer}}")"#
                    );
                }
                writeln!(output, "}};");
            });
            writeln!(output);

            writeln!(output, "public SubscriptionBuilder SubscriptionBuilder() => new(this);");
            writeln!(
                output,
                "public event Action<ReducerEventContext, Exception> OnUnhandledReducerError"
            );
            indented_block(output, |output| {
                writeln!(output, "add => Reducers.InternalOnUnhandledReducerError += value;");
                writeln!(output, "remove => Reducers.InternalOnUnhandledReducerError -= value;");
            });
        });

        vec![OutputFile {

View on GitHub (pinned to eddf9f5014)