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
- Set binding_mode to "client-server" (or the supported server mode) on the Python widget
- Check that the Python perspective package and the JS widget versions match, so both sides agree on supported binding modes
- 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 staleView on GitHub (pinned to 11c8238c0c)