clockworklabs/SpacetimeDB · error
unexpected update error: {e}
Error message
unexpected update error: {e} What it means
Raised by the bindings' update path (`ctx.db.<table>().<unique_index>().update(row)`) when `datastore_update_bsatn` returns ANY error. Unlike insert, there is no `TryUpdateError` yet — the source carries a `TODO(centril): introduce a TryUpdateError` — so every failure is a panic. The most common real trigger is an update that would violate a unique/identity constraint (for example changing a row's unique column to a value another row already holds).
Source
Thrown at crates/bindings/src/table.rs:1392
/// Update a row of type `T` to `row` using the index identified by `index_id`.
#[track_caller]
fn update<T: Table>(index_id: IndexId, mut row: T::Row, mut buf: IterBuf) -> T::Row {
let table_id = T::table_id();
// Encode the row as bsatn into the buffer `buf`.
buf.clear();
buf.serialize_into(&row).unwrap();
// Insert row into table.
// When table has an auto-incrementing column, we must re-decode the changed `buf`.
let res = sys::datastore_update_bsatn(table_id, index_id, &mut buf).map(|gen_cols| {
// Let the caller handle any generated columns written back by `sys::datastore_update_bsatn` to `buf`.
T::integrate_generated_columns(&mut row, gen_cols);
row
});
// TODO(centril): introduce a `TryUpdateError`.
res.unwrap_or_else(|e| panic!("unexpected update error: {e}"))
}
/// A table iterator which yields values of the `TableType` corresponding to the table.
struct TableIter<T: DeserializeOwned> {
/// The underlying source of our `Buffer`s.
inner: sys::RowIter,
/// The current position in the buffer, from which `deserializer` can read.
reader: Cursor<IterBuf>,
_marker: PhantomData<T>,
}
impl<T: DeserializeOwned> TableIter<T> {
#[inline]
fn new(iter: sys::RowIter) -> Self {
TableIter::new_with_buf(iter, IterBuf::take())
}View on GitHub (pinned to 6dee26c6ef)
Solutions
- Before updating, check whether the target value is already taken by another row: `ctx.db.user().email().filter(&new_email)` and only `update` when the hit is the row you are editing.
- If you control the workflow, replace update-on-unique-column with delete + `try_insert` so uniqueness violations surface as `TryInsertError` values you can handle.
- Ensure the module was republished after any schema change so the row layout matches.
- Watch the SpacetimeDB changelog for a `TryUpdateError` API; until then treat any other errno here (with matching versions) as a bug to report.
Example fix
// before
ctx.db.user().id().update(User { id, email: new_email, ..row });
// after
let taken = ctx.db.user().email().filter(&new_email).any(|u| u.id != id);
if taken {
Err("email already in use")?;
}
ctx.db.user().id().update(User { id, email: new_email, ..row }); Defensive patterns
Strategy: validation
Validate before calling
// Pre-check every unique column you are about to change:
let owner = ctx.db.user().email().filter(&new_email).next();
match owner {
Some(u) if u.id == row.id => { /* same row, safe */ }
Some(_) => return Err("email already in use".into()),
None => { /* safe to update */ }
}
ctx.db.user().id().update(updated_row); Try / catch
// No TryUpdateError exists yet: any update failure panics and rolls the
// reducer back. Validate first, and keep updates off hot conflict paths.
let attempted = std::panic::catch_unwind(AssertUnwindSafe(|| {
ctx.db.user().id().update(updated_row)
}));
attempted.expect("update failed; transaction aborted anyway"); Prevention
- Check uniqueness of any changed unique column with a filter before calling update.
- Model edit flows as delete + try_insert when uniqueness conflicts must be handled gracefully.
- Track the SpacetimeDB roadmap for a TryUpdateError API and migrate once available.
When it happens
Trigger: Calling `update` on a unique-index handle where the new row collides with a different existing row on that index (duplicate email/username), where an auto-increment column would overflow, or where the row encoding no longer matches the table schema.
Common situations: Profile-edit reducers that set a username/email without checking for an existing owner; schema changes republished without rebuilding the module; tests that update rows to values seeded elsewhere in the table.
Related errors
- When providing a name, you must also provide the object.
- wrong number of elements
- unexpected error from `datastore_delete_by_index_scan_range_
- unexpected error from `datastore_index_scan_range_bsatn`: {e
- unexpected insertion error: {e}
AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20).
Data as JSON: /api/errors/7a3f9fb9b001aa95.
Report an issue: GitHub.