slint-ui/slint · error

Callbacks were set up earlier

Error message

Callbacks were set up earlier

What it means

The WASM preview connector stores JS callbacks (lsp_notifier, resource_url_mapper, invoke_slintpad_callback) in a thread-local `WASM_CALLBACKS: RefCell<Option<WasmCallbacks>>` that starts as None and is only populated by `PreviewConnector::create`. When `WasmPreviewToLsp::send` runs, it expects that initialization already happened and panics via `.expect("Callbacks were set up earlier")` if the Option is still None. This is an internal initialization-order invariant: the LSP->preview channel can only be created by `create`, so callbacks must exist by the time messages flow.

Solutions

  1. Ensure the JS host calls and awaits the promise returned by `PreviewConnector.create(...)` before performing any action that triggers preview-to-LSP traffic (opening a file, editing, UI events).
  2. If wiring the connector manually, construct it exactly once per wasm module instance and reuse that instance, rather than creating a second PreviewConnector in the same module.
  3. Check the browser console for a JS error thrown during `create` (e.g. 'PreviewConnector already set up.') that left initialization incomplete, and fix that root cause.
  4. As a defensive patch, replace the expect in send with an early return/error so a missing notifier logs instead of panicking the wasm instance.

Example fix

// JS: before
const connector = new slint_lsp.PreviewConnector();
connector.create(notify, mapUrl, "fluent-dark", cb); // not awaited, sends happen too early

// JS: after
const connector = new slint_lsp.PreviewConnector();
await connector.create(notify, mapUrl, "fluent-dark", cb); // callbacks registered before use
// now safe to open files / trigger preview messages
Defensive patterns

Strategy: type-guard

Validate before calling

// JS, before any preview interaction
if (!connectorInitialized) {
  throw new Error("PreviewConnector.create() has not resolved yet");
}

Type guard

// JS
function callbacksReady(conn) {
  return conn && typeof conn.__lspNotifier === "function";
}

Try / catch

// wrap create and gate traffic
try {
  await connector.create(notify, mapUrl, style, cb);
  connectorInitialized = true;
} catch (e) {
  console.error("preview connector init failed", e);
}

Prevention

When it happens

Trigger: Calling `WasmPreviewToLsp::send` (i.e. the preview UI trying to notify the LSP side) before JS invoked `PreviewConnector.create(lspNotifier, resourceUrlMapper, style, invokeSlintpadCallback)`, or after a failed/rejected `create` that nonetheless left a partially constructed preview state that still emits messages.

Common situations: Integrating slintpad/slint LSP wasm into a custom JS shell where the connector object is created but `PreviewConnector.create` is not awaited before the UI sends an early message; a JS exception inside `create` rejecting the promise after callbacks were not set; double-loading the wasm module so a second instance's preview state sends through an uninitialized first instance.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of slint-ui/slint@bb937076de (2026-09-16). Data as JSON: /api/errors/ee5c524bb22c6d89. Report an issue: GitHub.

Appendix: source

Thrown at tools/lsp/connector/wasm.rs:279

impl editor_preview::LspToPreview for WasmLspToPreview {
    fn send(&self, message: &LspToPreviewMessage) {
        let _ = self.server_notifier.send_notification::<LspToPreviewMessage>(message.clone());
    }

    fn preview_target(&self) -> PreviewTarget {
        PreviewTarget::EmbeddedWasm
    }
}

#[derive(Default)]
struct WasmPreviewToLsp {}

impl editor_preview::PreviewToLsp for WasmPreviewToLsp {
    fn send(&self, message: &PreviewToLspMessage) -> editor_preview::Result<()> {
        WASM_CALLBACKS.with_borrow(|callbacks| {
            let notifier = js_sys::Function::from(
                (callbacks.as_ref().expect("Callbacks were set up earlier").lsp_notifier).clone(),
            );
            let value = serde_wasm_bindgen::to_value(&message)?;
            notifier
                .call1(&JsValue::UNDEFINED, &value)
                .map_err(|_| "Failed to send message to LSP".to_string())?;
            Ok(())
        })
    }
}

fn init_slintpad_specific_ui(api: &crate::preview::ui::Api) {
    if !WASM_CALLBACKS.with_borrow(|callbacks| {
        callbacks.as_ref().map_or(false, |cb| cb.invoke_slintpad_callback.is_function())
    }) {
        return;
    }

    api.set_runs_in_slintpad(true);

View on GitHub (pinned to bb937076de)