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

  1. 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.
  2. If you control the workflow, replace update-on-unique-column with delete + `try_insert` so uniqueness violations surface as `TryInsertError` values you can handle.
  3. Ensure the module was republished after any schema change so the row layout matches.
  4. 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

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


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/7a3f9fb9b001aa95. Report an issue: GitHub.