linebender/druid · error

unexpected wayland event

Error message

unexpected wayland event: {evt:?}

What it means

During `sync()`'s roundtrip, the Wayland connection expects only reply events, but the server sent an unexpected event before the roundtrip finished. Because pre-loop events cannot be routed to a window, the code panics rather than dropping them silently.

Solutions

  1. Only call `sync()` during setup, before the event loop starts
  2. Route events through the running loop's dispatcher after startup instead of syncing
  3. Update wayland-backend/compositor handling if a legit new event type arrives; patch the panic to log-and-ignore

Example fix

// before
let app = Application::new()?;
app.run(None);
app.sync()?; // loop already running

// after
let app = Application::new()?;
app.sync()?; // before run
app.run(None);
Defensive patterns

Strategy: validation

Validate before calling

// Guard sync behind a lifecycle check in caller code:
if !event_loop_started { app.sync()?; }

Type guard

fn sync_allowed(loop_started: bool) -> bool { !loop_started }

Prevention

When it happens

Trigger: Calling `sync()` after the event loop has started (its documented precondition), or the compositor dispatching an unhandled event type during a `sync_roundtrip` callback.

Common situations: Calling `Application::sync` at runtime after `run` started the loop; compositor quirks sending events during the initial roundtrip.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of linebender/druid@0f8b1195e4 (2026-09-10). Data as JSON: /api/errors/40649dd4d0f62651. Report an issue: GitHub.

Appendix: source

Thrown at druid-shell/src/backend/wayland/application.rs:446

    fn zwlr_layershell_v1(&self) -> Option<wl::Main<ZwlrLayerShellV1>> {
        self.zwlr_layershell_v1.clone()
    }
}

impl Data {
    pub(crate) fn set_cursor(&self, cursor: &mouse::Cursor) {
        self.pointer.replace(cursor);
    }

    /// Send all pending messages and process all received messages.
    ///
    /// Don't use this once the event loop has started.
    pub(crate) fn sync(&self) -> Result<(), Error> {
        self.wayland
            .queue
            .borrow_mut()
            .sync_roundtrip(&mut (), |evt, _, _| {
                panic!("unexpected wayland event: {evt:?}")
            })
            .map_err(Error::fatal)?;
        Ok(())
    }

    fn current_window_id(&self) -> u64 {
        static DEFAULT: u64 = 0_u64;
        *self.active_surface_id.borrow().front().unwrap_or(&DEFAULT)
    }

    pub(super) fn acquire_current_window(&self) -> Option<WindowHandle> {
        self.handles
            .borrow()
            .get(&self.current_window_id())
            .cloned()
    }

    fn handle_timer_event(&self, _token: TimerToken) {

View on GitHub (pinned to 0f8b1195e4)