clockworklabs/SpacetimeDB · error
Procedure return value failed to serialize to BSATN
Error message
Procedure return value failed to serialize to BSATN
What it means
When a procedure is called over WebSocket v1 with the binary protocol, the host serializes the return value with `bsatn::to_vec(...).expect("Procedure return value failed to serialize to BSATN")` before sending it only to the caller. BSATN encoding of a well-formed SpacetimeType is essentially infallible, so this panic indicates the return type violates serialization invariants — a hand-written Serialize that errors, or a malformed/mismatched runtime type after schema changes.
Source
Thrown at crates/core/src/client/messages.rs:848
let status = match status {
ProcedureStatus::InternalError(msg) => ws_v1::ProcedureStatus::InternalError(msg),
ProcedureStatus::OutOfEnergy => ws_v1::ProcedureStatus::OutOfEnergy,
ProcedureStatus::Returned(val) => ws_v1::ProcedureStatus::Returned(serialize_value(val)),
};
ws_v1::ServerMessage::ProcedureResult(ws_v1::ProcedureResult {
status,
timestamp,
total_host_execution_duration,
request_id,
})
}
// Note that procedure returns are sent only to the caller, not broadcast to all subscribers,
// so we don't have to bother with memoizing the serialization the way we do for reducer args.
match protocol {
Protocol::Binary => ws_v1::FormatSwitch::Bsatn(convert(self, |val| {
bsatn::to_vec(&val)
.expect("Procedure return value failed to serialize to BSATN")
.into()
})),
Protocol::Text => ws_v1::FormatSwitch::Json(convert(self, |val| {
serde_json::to_string(&SerializeWrapper(val))
.expect("Procedure return value failed to serialize to JSON")
.into()
})),
}
}
}
View on GitHub (pinned to 524b4487d9)
Solutions
- Republish the module so host and schema agree, then regenerate client bindings and reconnect.
- Unit-test the return type in the module crate: `bsatn::to_vec(&sample)` should succeed before deploy.
- Replace custom Serialize impls on return values with derived SpacetimeType impls.
- Align SDK/module/crate versions — version drift can produce malformed types.
Example fix
// before: hand-rolled Serialize on the return type can fail at runtime
fn get_cfg(ctx: &ReducerContext) -> CustomRet { CustomRet::bad_impl() }
// after: derive SpacetimeType and smoke-test encoding
#[derive(spacetimedb::SpacetimeType, Clone)]
pub struct CustomRet { pub level: u8 }
#[test]
fn ret_bsatn_ok() { assert!(spacetimedb_lib::bsatn::to_vec(&CustomRet { level: 3 }).is_ok()); } Defensive patterns
Strategy: type-guard
Validate before calling
// Module-side test: prove every return value encodes before deploying
#[test]
fn procedure_returns_encode() {
let sample = make_sample_return();
assert!(spacetimedb_lib::bsatn::to_vec(&sample).is_ok());
} Type guard
fn assert_bsatn_serializable<T: spacetimedb_lib::bsatn::Serialize>() {}
// invoke in module init: assert_bsatn_serializable::<MyRet>(); Prevention
- Derive SpacetimeType for all procedure return types; never hand-write Serialize for them.
- Add serialization smoke tests for public return shapes in CI.
- Regenerate client bindings and republish after every module change before connecting.
When it happens
Trigger: Calling a procedure whose return type fails BSATN encoding: custom Serialize impls returning errors, schema/runtime skew after changing the module without republishing, or SDK/macro version drift producing malformed types.
Common situations: Editing a module's return type and connecting with stale client bindings; hand-rolled Serialize impls on return values; mismatched spacetimedb crate versions between module and SDK.
Related errors
- Procedure return value failed to serialize to BSATN
- Procedure return value failed to serialize to JSON
- cannot serialize refs without a typespace
- could not serialize result: object had neither a `ok` nor an
- Could not serialize sum type; unknown tag ${value.tag}
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/277020a0ef6d67c9.
Report an issue: GitHub.