tauri-apps/tauri · error

`Window::set_enabled` can only be called on the main thread

Error message

`Window::set_enabled` can only be called on the main thread

What it means

macOS UI APIs are main-thread-only. In tauri-runtime-wry's macOS backend, Window::set_enabled builds the sheet overlay via MainThreadMarker::new().expect("`Window::set_enabled` can only be called on the main thread"); MainThreadMarker::new() returns None when called off the main thread, so invoking set_enabled from any other thread panics.

Source

Thrown at crates/tauri-runtime-wry/src/window/macos.rs:17

// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use objc2::MainThreadMarker;
use objc2_app_kit::{NSBackingStoreType, NSWindow, NSWindowStyleMask};
use tao::platform::macos::WindowExtMacOS;

impl super::WindowExt for tao::window::Window {
  // based on electron implementation
  // https://github.com/electron/electron/blob/15db63e26df3e3d59ce6281f030624f746518511/shell/browser/native_window_mac.mm#L474
  fn set_enabled(&self, enabled: bool) {
    let ns_window: &NSWindow = unsafe { &*self.ns_window().cast() };
    if !enabled {
      let frame = ns_window.frame();
      let mtm = MainThreadMarker::new()
        .expect("`Window::set_enabled` can only be called on the main thread");
      let sheet = unsafe {
        NSWindow::initWithContentRect_styleMask_backing_defer(
          mtm.alloc(),
          frame,
          NSWindowStyleMask::Titled,
          NSBackingStoreType::Buffered,
          false,
        )
      };
      sheet.setAlphaValue(0.5);
      ns_window.beginSheet_completionHandler(&sheet, None);
    } else if let Some(attached) = ns_window.attachedSheet() {
      ns_window.endSheet(&attached);
    }
  }

  fn is_enabled(&self) -> bool {
    let ns_window: &NSWindow = unsafe { &*self.ns_window().cast() };

View on GitHub (pinned to 52e4b6e71d)

Solutions

  1. Dispatch the call to the main thread: window.run_on_main_thread(move || { window.set_enabled(false); }) or app_handle.run_on_main_thread(...)
  2. Restructure so UI mutations happen in main-thread contexts (event handlers, non-async commands)
  3. Guard UI code with a main-thread assertion in debug builds to fail early with a clear message
  4. Audit async blocks that capture window handles for direct API calls

Example fix

// before
tauri::async_runtime::spawn(async move {
  window.set_enabled(false); // panics off-main-thread on macOS
  do_work().await;
});

// after
let w = window.clone();
window.run_on_main_thread(move || w.set_enabled(false))?;
tauri::async_runtime::spawn(async move { do_work().await });
Defensive patterns

Strategy: type-guard

Validate before calling

// call sites guard before touching UI: if !is_main_thread() { window.run_on_main_thread(...)?; }

Type guard

use std::sync::OnceLock;
static MAIN_THREAD: OnceLock<std::thread::ThreadId> = OnceLock::new();
pub fn init_main_thread() { MAIN_THREAD.set(std::thread::current().id()).ok(); }
pub fn is_main_thread() -> bool {
    MAIN_THREAD.get().is_some_and(|id| *id == std::thread::current().id())
}

Prevention

When it happens

Trigger: Calling window.set_enabled(false/true) from a tokio task, std::thread, async command handler, or other non-main thread — e.g. disabling the window while awaiting a long operation — without dispatching to the main thread.

Common situations: Porting synchronous window logic to async (spawn(async move { window.set_enabled(false); ... })); calling window APIs from background workers; WebDriver/test harnesses touching windows from helper threads.

Related errors


AI-assisted analysis of tauri-apps/tauri@52e4b6e71d (2026-08-20). Data as JSON: /api/errors/9c96ce11e439b687. Report an issue: GitHub.