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
- Dispatch the call to the main thread: window.run_on_main_thread(move || { window.set_enabled(false); }) or app_handle.run_on_main_thread(...)
- Restructure so UI mutations happen in main-thread contexts (event handlers, non-async commands)
- Guard UI code with a main-thread assertion in debug builds to fail early with a clear message
- 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
- Route every Window/WebviewWindow mutation through run_on_main_thread on macOS
- Audit async blocks and background threads that capture window handles
- Add debug_assert!(is_main_thread()) inside UI helper functions
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
- view to be installed in window
- asset protocol path "{path}" is not valid: {e}
- File does not exist at path: {path}
- failed to read plist {}: {}
- Invalid SDK root {}
AI-assisted analysis of tauri-apps/tauri@52e4b6e71d (2026-08-20).
Data as JSON: /api/errors/9c96ce11e439b687.
Report an issue: GitHub.