clockworklabs/SpacetimeDB · error

NO_SUCH_REDUCER

NO_SUCH_REDUCER

Error message

ERROR: Invalid reducer ID %u (have %zu reducers)\n

What it means

Dispatch guard in Module::__call_reducer__: the host invoked a reducer by numeric ID >= the number of reducer handlers registered at module init (g_reducer_handlers). The handler writes 'Invalid reducer ID: <id>' into the caller's error sink and returns StatusCode::NO_SUCH_REDUCER (999), which the client surfaces as a failed reducer call. It means the server's reducer table (captured at publish time) and the binary's registered handlers disagree.

Source

Thrown at crates/bindings-cpp/src/internal/Module.cpp:474

    
    size_t bytes_to_write = bytes.size();
    FFI::bytes_sink_write(sink, bytes.data(), &bytes_to_write);
}

Status Module::__call_reducer__(
    uint32_t id,
    uint64_t sender_0, uint64_t sender_1, uint64_t sender_2, uint64_t sender_3,
    uint64_t conn_id_0, uint64_t conn_id_1,
    Timestamp timestamp,
    BytesSource args_source,
    BytesSink error_sink
) {
    // Clear any previous error state
    SpacetimeDB::Internal::clear_reducer_error();

    // Check if reducer ID is valid
    if (id >= g_reducer_handlers.size()) {
        fprintf(stderr, "ERROR: Invalid reducer ID %u (have %zu reducers)\n", 
                id, g_reducer_handlers.size());
        
        // Write error message
        std::string error = "Invalid reducer ID: " + std::to_string(id);
        WriteBytes(error_sink, std::vector<uint8_t>(error.begin(), error.end()));
        return StatusCode::NO_SUCH_REDUCER;
    }
    
    // Create reducer context
    std::array<uint8_t, 32> sender_bytes{};
    // Pack the 4 uint64_t parts into 32 bytes
    std::memcpy(sender_bytes.data(), &sender_0, 8);
    std::memcpy(sender_bytes.data() + 8, &sender_1, 8);
    std::memcpy(sender_bytes.data() + 16, &sender_2, 8);
    std::memcpy(sender_bytes.data() + 24, &sender_3, 8);
    
    Identity sender_identity(sender_bytes);
    

View on GitHub (pinned to 6dee26c6ef)

Solutions

  1. Republish the module so the server's reducer table matches the binary, then retry the call
  2. Make every reducer registration macro unconditional (no runtime branches around registration) so the handler count is deterministic
  3. Check server logs for preinit/registration errors at module startup that could have skipped handlers
  4. Verify the client calls reducers by the exact registered names and uses freshly generated bindings after schema changes

Example fix

// before
REDUCER(create_user) { /* ... */ }
#ifndef DEBUG
REDUCER(audit_log) { /* ... */ } // conditionally absent -> id shift
#endif

// after
REDUCER(create_user) { /* ... */ }
REDUCER(audit_log) { /* ... */ } // always registered, stable ids
Defensive patterns

Strategy: validation

Validate before calling

// post-build test: handler counts must match the expected, published module
// assert(Module::GetReducerHandlerCount() == EXPECTED_REDUCERS);
// and assert every expected name is present in __describe_module__ output

Prevention

When it happens

Trigger: The deployed wasm binary does not match the module description the server dispatches against (partial/failed publish or stale binary); reducer registration macros compiled out or wrapped in runtime conditionals so fewer handlers register; an earlier preinit error aborted part of registration; name-index drift after adding/removing reducers between the client's expectation and the published module.

Common situations: Publishing a new module version while old clients/server metadata cache the previous reducer list; #ifdef-ing out a reducer in one build configuration; init failure swallowed before all __preinit__30_* reducer registration functions ran.

Related errors


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