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

  1. Rebuild the module with the spacetimedb SDK/CLI version that matches the server, clearing stale artifacts under target/ first, then republish.
  2. 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.
  3. Verify CLI, SDK, and server version alignment (`spacetime --version`, server version endpoint) and read release notes for ABI-breaking changes.
  4. 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

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


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