clockworklabs/SpacetimeDB · critical
unable to decode args
Error message
unable to decode args
What it means
SpacetimeDB's module runtime (crates/bindings/src/rt.rs) decodes a reducer's arguments from the BSATN wire format before invoking: bsatn::from_slice::<SerDeArgs>(args). The result is unwrapped with .expect("unable to decode args") — a decode failure PANICS the module call rather than returning an error. Decode fails when the bytes do not match the reducer's expected argument tuple layout (wrong arity, types, or truncated/extra bytes).
Solutions
- Regenerate the client bindings/SDK from the currently deployed module version and retry the call
- Redeploy so module and clients are built from the same schema; coordinate a version bump so old clients fail fast with a clear schema-mismatch instead
- Verify the bytes being sent are BSATN of the exact argument tuple — encode with the same generated Args types on the client
- Inspect server logs for the panic backtrace to identify which reducer/arg failed, then diff that schema between client and module
Example fix
// before (client, stale SDK: callReducer('add_user', bsatnEncode({ name: 'x' })) against module
// whose reducer is fn add_user(ctx, name: String, age: u32) -> panics: unable to decode args
// after: regenerate SDK from the deployed module, then
const res = await db.reducers.addUser('x', 30); // arity & types now match the module schema Defensive patterns
Strategy: validation
Validate before calling
// Client-side: encode args with the SAME generated types that will decode them.
// If you must pre-validate raw bytes, decode them against the expected schema first:
let SerDeArgs(_) = spacetimedb::bsatn::from_slice(args)
.map_err(|e| anyhow::anyhow!("client args would fail module decode: {e}"))?; Try / catch
// This is .expect() -> a panic, not a Result. There is no in-module catch:
// guard at the boundary. Hosts should catch_unwind / trap the wasm call:
let res = std::panic::catch_unwind(AssertUnwindSafe(|| invoke_reducer(...)));
if res.is_err() { /* report schema mismatch for this reducer to the client */ } Prevention
- Regenerate client SDKs whenever the module is redeployed and bump versions together
- Add a CI check comparing client-generated schema hash to deployed module schema hash
- Always encode reducer args with generated Args types, never by hand
- For no-arg reducers send the canonical empty-tuple BSATN encoding
When it happens
Trigger: A client invoking the reducer with BSATN args encoded against a different module schema than the one deployed: argument count changed, a field type changed (u32 -> u64, string -> bytes), an enum/table row type renamed, or the payload is truncated / not BSATN at all. Deploying a new module version while old clients keep sending the old encoding.
Common situations: Client SDK generated from an older module; module updated (even a benign-looking type change) without regenerating bindings; hand-rolled encoders using JSON or a different serialization instead of BSATN; partial network writes corrupting the arg buffer; version skew between spacetimedb server, module, and SDK.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- unable to encode view return value
- a row was a sequence trigger but there was no generated…
- heterogeneous array
- cannot deserialize refs without a typespace
- Cannot serialize a null string as BSATN String. To…
AI-assisted analysis of clockworklabs/SpacetimeDB@eddf9f5014 (2026-08-20).
Data as JSON: /api/errors/90694fad77c37610.
Report an issue: GitHub.
Appendix: source
Thrown at crates/bindings/src/rt.rs:54
}
impl<T> IntoVec<T> for Option<T> {
fn into_vec(self) -> Vec<T> {
self.into_iter().collect()
}
}
/// The `sender` invokes `reducer` at `timestamp` and provides it with the given `args`.
///
/// Returns an invalid buffer on success
/// and otherwise the error is written into the fresh one returned.
pub fn invoke_reducer<'a, A: Args<'a>>(
reducer: impl Reducer<'a, A>,
ctx: &ReducerContext,
args: &'a [u8],
) -> Result<(), Box<str>> {
// Deserialize the arguments from a bsatn encoding.
let SerDeArgs(args) = bsatn::from_slice(args).expect("unable to decode args");
reducer.invoke(ctx, args)
}
pub fn invoke_procedure<'a, A: Args<'a>, Ret: IntoProcedureResult>(
procedure: impl Procedure<'a, A, Ret>,
ctx: &mut ProcedureContext,
args: &'a [u8],
) -> ProcedureResult {
// Deserialize the arguments from a bsatn encoding.
let SerDeArgs(args) = bsatn::from_slice(args).expect("unable to decode args");
let res = procedure.invoke(ctx, args);
res.to_result()
}
/// A trait for types representing the *execution logic* of a reducer.View on GitHub (pinned to eddf9f5014)