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
- Regenerate the C# bindings from the deployed module version (`spacetime generate --lang csharp --out-dir ... --project-path <module>`) and ship a client update
- 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
- Verify each client connects to the database version its bindings were generated from
- 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
- Run `spacetime generate` in the same pipeline step that publishes the module
- Pin clients to a known database/module version and version-check at connect time
- Treat new reducers as a coordinated server+client release
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
- Invalid tag value, this state should be unreachable.
- Transaction context was not initialised.
- Unknown HTTP version
- a row was a sequence trigger but there was no generated…
- Argument must be a ConnectionId
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)