clockworklabs/SpacetimeDB · error
expected ModuleDef to contain key, but it does not
Error message
expected ModuleDef to contain key, but it does not
What it means
Panic from `ModuleDef::lookup_expect`, SpacetimeDB's schema-module convenience lookup. It calls `T::lookup(self, key)` and `.expect("expected ModuleDef to contain key, but it does not")` when the key (table/view/sequence/constraint/type name, possibly namespace-qualified) is absent. The API contract states: only call it when existence is guaranteed (e.g. the key came from iterating the same ModuleDef); a panic means an unverified lookup assumption.
Solutions
- Replace the `lookup_expect` call with `lookup(...).ok_or_else(|| anyhow!("...not found..."))?` so callers receive a real error instead of a panic (the codebase already does this nearby for AddConstraint lookups).
- Verify the key's provenance: keys must come from the same ModuleDef instance or be re-validated after any publish/migration boundary.
- Print/log the exact key (and its namespace path) at failure to spot join/casing mismatches between constructed names and stored identifiers.
- Add a regression test covering the offending operation (e.g. the migration step) with the failing key.
Example fix
// before: panics on missing key
let table = module_def.lookup_expect::<TableDef>(key);
// after: return a typed error
let table = module_def
.lookup::<TableDef>(key)
.ok_or_else(|| anyhow::anyhow!("table `{key}` not found in module def"))?; Defensive patterns
Strategy: validation
Validate before calling
// Replace lookup_expect with a checked lookup at the call boundary
let def = module_def
.lookup::<TableDef>(key)
.ok_or_else(|| anyhow::anyhow!("`{key}` not found in module def"))?; Type guard
// Narrow before use when the key type is dynamic
fn has_table(md: &ModuleDef, name: &str) -> bool { md.table(name).is_some() } Try / catch
let r = std::panic::catch_unwind(AssertUnwindSafe(|| md.lookup_expect::<T>(key)));
match r { Ok(v) => v, Err(_) => return Err(SchemaError::key_not_found(key)) } Prevention
- Only call lookup_expect with keys obtained by iterating the SAME ModuleDef.
- Prefer `lookup(...).ok_or(anyhow!(...))?` — the codebase itself uses error returns for the neighboring AddConstraint case.
- Log the exact key (with namespace path) when lookups fail to catch join/casing mismatches.
When it happens
Trigger: Calling `lookup_expect` with a key obtained from a DIFFERENT module version (old plan vs new def during migration), a name built by string concatenation that doesn't match an actual entry (submodule path join mismatch, casing), or a key validated against one collection but looked up in another (e.g. constraint name found in the wrong namespace map).
Common situations: Migration code that resolves names from a `DatabaseDef`/plan into a freshly published `ModuleDef` where names shifted; refactors renaming entities while stale references remain; submodule namespace-prefix inconsistencies; cross-crate code assuming another crate added a definition. Note the sibling code in update.rs:486 catches a similar case with a proper `anyhow!` error instead — the ideal fix direction.
Related errors
- Failed to generate table due to validation errors
- table not found in old_module_def
- table schema should validate for query builder codegen
- table should exist in the database for AddConstraint
- Duration overflows i64 microseconds
AI-assisted analysis of clockworklabs/SpacetimeDB@eddf9f5014 (2026-08-20).
Data as JSON: /api/errors/72c89854d8353758.
Report an issue: GitHub.
Appendix: source
Thrown at crates/schema/src/def.rs:590
/// The `TableDef` an entity in the global namespace is stored in, if any.
///
/// Generally, you will want to use the `lookup` method on the entity type instead.
pub fn stored_in_table_def(&self, name: &RawIdentifier) -> Option<&TableDef> {
self.stored_in_table_def
.get(name)
.and_then(|table_name| self.tables.get(table_name))
}
/// Lookup a definition by its key in `self`.
pub fn lookup<T: ModuleDefLookup>(&self, key: T::Key<'_>) -> Option<&T> {
T::lookup(self, key)
}
/// Lookup a definition by its key in `self`, panicking if not found.
/// Only use this method if you are sure the key exists in the module definition.
pub fn lookup_expect<T: ModuleDefLookup>(&self, key: T::Key<'_>) -> &T {
T::lookup(self, key).expect("expected ModuleDef to contain key, but it does not")
}
/// Convenience method to look up a table, possibly by a string.
pub fn table<K: ?Sized + Hash + Equivalent<Identifier>>(&self, name: &K) -> Option<&TableDef> {
// If the string IS a valid identifier, we can just look it up.
self.tables.get(name)
}
/// Convenience method to look up a view, possibly by a string.
pub fn view<K: ?Sized + Hash + Equivalent<Identifier>>(&self, name: &K) -> Option<&ViewDef> {
// If the string IS a valid identifier, we can just look it up.
self.views.get(name)
}
/// Convenience method to look up a view, possibly by a string, returning its id as well.
pub fn view_full<K: ?Sized + Hash + Equivalent<Identifier>>(&self, name: &K) -> Option<(ViewFnPtr, &ViewDef)> {
// If the string IS a valid identifier, we can just look it up.
self.views.get(name).map(|def| (def.fn_ptr, def))View on GitHub (pinned to eddf9f5014)