{"record":{"id":"7a3f9fb9b001aa95","repo":"clockworklabs/SpacetimeDB","slug":"unexpected-update-error-e","errorCode":null,"errorMessage":"unexpected update error: {e}","messagePattern":"unexpected update error: (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/bindings/src/table.rs","lineNumber":1392,"sourceCode":"\n/// Update a row of type `T` to `row` using the index identified by `index_id`.\n#[track_caller]\nfn update<T: Table>(index_id: IndexId, mut row: T::Row, mut buf: IterBuf) -> T::Row {\n    let table_id = T::table_id();\n    // Encode the row as bsatn into the buffer `buf`.\n    buf.clear();\n    buf.serialize_into(&row).unwrap();\n\n    // Insert row into table.\n    // When table has an auto-incrementing column, we must re-decode the changed `buf`.\n    let res = sys::datastore_update_bsatn(table_id, index_id, &mut buf).map(|gen_cols| {\n        // Let the caller handle any generated columns written back by `sys::datastore_update_bsatn` to `buf`.\n        T::integrate_generated_columns(&mut row, gen_cols);\n        row\n    });\n\n    // TODO(centril): introduce a `TryUpdateError`.\n    res.unwrap_or_else(|e| panic!(\"unexpected update error: {e}\"))\n}\n\n/// A table iterator which yields values of the `TableType` corresponding to the table.\nstruct TableIter<T: DeserializeOwned> {\n    /// The underlying source of our `Buffer`s.\n    inner: sys::RowIter,\n\n    /// The current position in the buffer, from which `deserializer` can read.\n    reader: Cursor<IterBuf>,\n\n    _marker: PhantomData<T>,\n}\n\nimpl<T: DeserializeOwned> TableIter<T> {\n    #[inline]\n    fn new(iter: sys::RowIter) -> Self {\n        TableIter::new_with_buf(iter, IterBuf::take())\n    }","sourceCodeStart":1374,"sourceCodeEnd":1410,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/6dee26c6efc2856793e12b148a59742964f5d783/crates/bindings/src/table.rs#L1374-L1410","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nctx.db.user().id().update(User { id, email: new_email, ..row });\n\n// after\nlet taken = ctx.db.user().email().filter(&new_email).any(|u| u.id != id);\nif taken {\n    Err(\"email already in use\")?;\n}\nctx.db.user().id().update(User { id, email: new_email, ..row });","handlingStrategy":"validation","validationCode":"// Pre-check every unique column you are about to change:\nlet owner = ctx.db.user().email().filter(&new_email).next();\nmatch owner {\n    Some(u) if u.id == row.id => { /* same row, safe */ }\n    Some(_) => return Err(\"email already in use\".into()),\n    None => { /* safe to update */ }\n}\nctx.db.user().id().update(updated_row);","typeGuard":null,"tryCatchPattern":"// No TryUpdateError exists yet: any update failure panics and rolls the\n// reducer back. Validate first, and keep updates off hot conflict paths.\nlet attempted = std::panic::catch_unwind(AssertUnwindSafe(|| {\n    ctx.db.user().id().update(updated_row)\n}));\nattempted.expect(\"update failed; transaction aborted anyway\");","preventionTips":["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."],"tags":["spacetimedb","update","unique-constraint","reducer-panic"],"backgroundTag":"unique-constraint-violation","analyzedSha":"6dee26c6efc2856793e12b148a59742964f5d783","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}