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
- 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).
- 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.
- 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.
- 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
- Always await the PreviewConnector.create promise before sending or triggering preview messages.
- Create the connector exactly once per wasm module instance.
- Gate all UI actions and file opens behind the initialized flag.
- Watch the console for 'PreviewConnector already set up.' rejections.
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
- Cannot proceed without WebGL - aborting
- Child has no stdin
- Child has no stdout
- Could not find executable name of the slint-lsp
- {e:?}
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)