perspective-dev/perspective · error · Error

unknown binding mode

Error message

unknown binding mode: ${binding_mode}

What it means

render() branches on the widget's `binding_mode` trait to decide between client-server and server-bound table wiring; when the trait holds any value other than the supported "client-server" (e.g. a typo or an outdated kernel/frontend version mismatch), no code path exists and the render aborts.

Solutions

  1. Set binding_mode to "client-server" (or the supported server mode) on the Python widget
  2. Check that the Python perspective package and the JS widget versions match, so both sides agree on supported binding modes
  3. Inspect the received value (console.log(binding_mode)) to spot typos like "client_server"
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at packages/anywidget/src/js/index.js:141 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of perspective-dev/perspective@11c8238c0c (2026-09-09). Data as JSON: /api/errors/05f861fcc57cc2b2. Report an issue: GitHub.

Appendix: source

Thrown at packages/anywidget/src/js/index.js:141

    };
    model.on("msg:custom", on_custom_msg);
    model.send({ type: "connect", client_id }, null);

    const binding_mode = model.get("binding_mode");
    const table_name = model.get("table_name");
    if (!table_name) {
        throw new Error("table_name not set in model");
    }

    const table_promise = psp_client.open_table(table_name).then(async (t) => {
        if (binding_mode === "client-server") {
            const local_client = await perspective.worker();
            const remote_view = await t.view();
            return await local_client.table(remote_view);
        } else if (binding_mode === "server") {
            return t;
        } else {
            throw new Error(`unknown binding mode: ${binding_mode}`);
        }
    });

    // The viewer's `load()` also accepts a `Client` (with `table` naming the
    // binding in `ViewerConfig`) since the workspace->viewer merge, but the
    // single-table widget loads a `Table` directly and treats `table_name` as
    // the source of truth. The config's derived `table` field therefore has no
    // widget trait and is excluded from the config sync below. Revisit if the
    // widget ever grows multi-table (master/detail, global filter) support.
    await viewer.load(table_promise);
    await viewer.restore(
        Object.fromEntries(PERSISTENT_ATTRIBUTES.map((k) => [k, model.get(k)])),
    );

    // Bidirectional config sync as a single serialized reconciler. The viewer
    // (`perspective-config-update`) and the model (`change:`) each request a
    // reconcile in a fixed direction, and tasks run one at a time on
    // `reconcile` so a `restore()` never overlaps a `save()` read (no stale

View on GitHub (pinned to 11c8238c0c)