clockworklabs/SpacetimeDB · error
reducer transaction update {request_id} failed: {err}
Error message
reducer transaction update {request_id} failed: {err} What it means
In recv_reducer_update (crates/testing/src/modules.rs:153), a transaction update that carries the correct request_id is inspected for its EventStatus. FailedUser means the module code itself returned an Err from the reducer; FailedInternal means the host/runtime aborted the reducer (panic, unsupported operation, constraint violation at the runtime level). The {err} payload is the reducer's error message or the internal failure reason, so the transaction did not commit.
Source
Thrown at crates/testing/src/modules.rs:153
.recv_message()
.await
.ok_or_else(|| anyhow::anyhow!("client receiver closed before reducer update {request_id}"))?;
let OutboundMessage::V1(SerializableMessage::TxUpdate(update)) = message else {
anyhow::bail!("expected reducer transaction update {request_id}, got {message:?}");
};
let Some(event) = update.event else {
anyhow::bail!("expected full reducer transaction update {request_id}, got light update");
};
if event.request_id != Some(request_id) {
anyhow::bail!(
"expected reducer transaction update {request_id}, got request id {:?}",
event.request_id
);
}
match &event.status {
EventStatus::Committed(_) => Ok(()),
EventStatus::FailedUser(err) | EventStatus::FailedInternal(err) => {
anyhow::bail!("reducer transaction update {request_id} failed: {err}")
}
EventStatus::OutOfEnergy => anyhow::bail!("reducer transaction update {request_id} ran out of energy"),
}
}
pub async fn read_log(&self, size: Option<u32>) -> String {
let bytes = self
.client
.module()
.database_logger()
.tail(size, false)
.await
.unwrap()
.try_collect::<BytesMut>()
.await
.expect("failed to collect log stream");
String::from_utf8(bytes.into()).unwrap()
}View on GitHub (pinned to fb7282411b)
Solutions
- Read the {err} text: for FailedUser it is exactly the error your reducer returned - fix the module logic or the test input that violates the precondition.
- If the failure is expected, assert on it instead of using ? on recv_reducer_update: match the error and check the message, or consume the update yourself and match EventStatus::FailedUser.
- For FailedInternal, dump the module log with module.read_log(None) to find the runtime cause (panic location, constraint name).
- Add the missing fixture rows / context so the reducer's preconditions hold before calling it.
Example fix
// before: treats expected failure as test error
module.recv_reducer_update(rid).await?; // Err: reducer transaction update failed: `insufficient funds`
// after: assert the failure explicitly
let err = module.recv_reducer_update(rid).await.unwrap_err();
assert!(err.to_string().contains("insufficient funds"), "unexpected error: {err:#}"); Defensive patterns
Strategy: try-catch
Try / catch
// Expecting a deliberate failure: assert on the bail, don't propagate it.
let err = module.recv_reducer_update(rid).await.unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("failed:"), "expected reducer failure, got: {msg}");
assert!(msg.contains("insufficient funds"), "unexpected reason: {msg}"); Prevention
- Seed all fixture rows a reducer needs before calling it; most FailedUser errors are precondition checks.
- For reducers that may legitimately fail, assert on the failure text instead of using `?` on recv_reducer_update.
- On unexpected FailedInternal, dump module.read_log(None) first - the panic/host cause is in the module log, not the error.
When it happens
Trigger: A test calls a reducer that executes `return Err(...)` in module code; a reducer hits a unique constraint or precondition and errors intentionally; the module panics or performs an unsupported host call, yielding FailedInternal; test fixtures missing rows the reducer requires.
Common situations: Asserting success on reducers designed to fail (validation paths); forgetting fixture setup so a reducer's precondition check errors; FailedInternal after upgrading the module SDK where a host API changed behavior.
Related errors
- reducer transaction update {request_id} ran out of energy
- cannot serialize refs without a typespace
- cannot deserialize refs without a typespace
- There is already a reducer, procedure, or view with the name
- Table ${tableName} defines a schedule, but it seems like the
AI-assisted analysis of clockworklabs/SpacetimeDB@fb7282411b (2026-08-20).
Data as JSON: /api/errors/705dcc55dd29c38d.
Report an issue: GitHub.