clockworklabs/SpacetimeDB · critical
{CALL_PROCEDURE_DUNDER} export is a function with incorrect
Error message
{CALL_PROCEDURE_DUNDER} export is a function with incorrect type: {err} What it means
SpacetimeDB's wasmtime host resolves the `__call_procedure__` export on every instantiated module and converts it to a TypedFunc via `Func::typed`. The comment in get_call_procedure explains that wasmtime reports typing failures as opaque anyhow errors, so the host hand-rolls the lookup and panics when typing fails. This panic means the export exists and is a function, but its wasm signature does not match `CallProcedureType` (aliased to `CallReducerType`: a u32 ReducerId followed by the sender and argument parameters). It is almost always a module/host ABI mismatch, not a fault in your reducer logic.
Source
Thrown at crates/core/src/host/wasmtime/wasmtime_module.rs:457
/// Panics if the `instance` has an export at the expected name,
/// but it is not a function or is a function of an inappropriate type.
/// For new modules, this will be caught during publish.
/// Old modules from before the introduction of procedures might have an export at that name,
/// but it follows the double-underscore pattern of reserved names,
/// so we're fine to break those modules.
fn get_call_procedure(store: &mut Store<WasmInstanceEnv>, instance: &Instance) -> Option<CallProcedureType> {
// Wasmtime uses `anyhow` for error reporting, vexing library users the world over.
// This means we can't distinguish between the failure modes of `Instance::get_typed_func`.
// Instead, we type out the body of that method ourselves,
// but with error handling appropriate to our needs.
let export = instance.get_export(store.as_context_mut(), CALL_PROCEDURE_DUNDER)?;
Some(
export
.into_func()
.unwrap_or_else(|| panic!("{CALL_PROCEDURE_DUNDER} export is not a function"))
.typed(store)
.unwrap_or_else(|err| panic!("{CALL_PROCEDURE_DUNDER} export is a function with incorrect type: {err}")),
)
}
/// Look up the `instance`'s export named by [`CALL_VIEW_DUNDER`].
///
/// Similar to [`get_call_procedure`], but for views.
fn get_call_view(store: &mut Store<WasmInstanceEnv>, instance: &Instance) -> Option<CallViewType> {
let export = instance.get_export(store.as_context_mut(), CALL_VIEW_DUNDER)?;
Some(
export
.into_func()
.unwrap_or_else(|| panic!("{CALL_VIEW_DUNDER} export is not a function"))
.typed(store)
.unwrap_or_else(|err| panic!("{CALL_VIEW_DUNDER} export is a function with incorrect type: {err}")),
)
}
/// Look up the `instance`'s export named by [`CALL_VIEW_ANON_DUNDER`].View on GitHub (pinned to 6dee26c6ef)
Solutions
- Rebuild the module with the spacetimedb SDK/CLI version that matches the server, clearing stale artifacts under target/ first, then republish.
- Inspect the export: `wasm-objdump -x module.wasm` (or `wasm2wat module.wasm`) and compare `__call_procedure__` param/result types against the signature your server version expects.
- Verify CLI, SDK, and server version alignment (`spacetime --version`, server version endpoint) and read release notes for ABI-breaking changes.
- If hand-authoring wasm, copy the exact export signature emitted by the official SDK for your server version.
Example fix
// before: publish a module compiled against an older SDK spacetime publish my-db // after: align SDK + CLI with the server, rebuild clean, republish cargo update -p spacetimedb --precise <version-matching-server> rm -rf target/wasm32-* spacetime build && spacetime publish my-db -y
Defensive patterns
Strategy: type-guard
Validate before calling
use wasmtime::{Engine, Module, ExternType};
/// Reject modules whose dunder entry exports exist but are not functions.
fn check_export_kinds(engine: &Engine, wasm: &[u8]) -> Result<(), String> {
let module = Module::new(engine, wasm).map_err(|e| e.to_string())?;
for name in ["__call_procedure__", "__call_view__", "__call_view_anon__", "__call_http_handler__"] {
if let Some(exp) = module.get_export(name) {
if !matches!(exp.ty(), ExternType::Func(_)) {
return Err(format!("{name} exported as non-function"));
}
// additionally compare exp.ty() params/results against the server's expected TypedFunc signature
}
}
Ok(())
} Type guard
fn is_func_export(ty: &ExternType) -> bool { matches!(ty, ExternType::Func(_)) } Try / catch
// last resort at the embedding boundary
let inst = std::panic::catch_unwind(|| instantiate_module(module));
if let Err(p) = inst {
if panic_message(&p).contains("__call_procedure__") {
// ABI mismatch: rebuild the module with a matching SDK; do not retry as-is
}
} Prevention
- Pin the module SDK and spacetimedb server to a verified-compatible pair in CI.
- Smoke-test `spacetime publish` against a staging server after every toolchain upgrade.
- Never hand-write the `__call_*__` dunder exports; generate them with spacetimedb-bindgen.
- Verify exported signatures with wasm-objdump before deploying hand-modified wasm.
When it happens
Trigger: Instantiating or publishing a wasm module whose `__call_procedure__` export has a different signature than the TypedFunc<(u32 /* ReducerId */, sender, args, ...)> expected by this server build; reached on publish, database start/restart, or any call that instantiates the module host.
Common situations: Module artifact built with an SDK older or newer than the running spacetimedb server (bindgen emits a different entry signature); publishing a stale wasm under target/ after upgrading the CLI or SDK; hand-written WAT/wasm exporting the dunder name with wrong parameters.
Related errors
- {CALL_VIEW_DUNDER} export is a function with incorrect type:
- {CALL_VIEW_ANON_DUNDER} export is a function with incorrect
- {CALL_HTTP_HANDLER_DUNDER} export is a function with incorre
- {CALL_PROCEDURE_DUNDER} export is not a function
- {CALL_VIEW_DUNDER} export is not a function
AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20).
Data as JSON: /api/errors/84f413631df8b736.
Report an issue: GitHub.