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

  1. Regenerate the client bindings/SDK from the currently deployed module version and retry the call
  2. 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
  3. Verify the bytes being sent are BSATN of the exact argument tuple — encode with the same generated Args types on the client
  4. 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

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

Related errors


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)