bevyengine/bevy · error

window.canvas() can only be called in main thread.

Error message

window.canvas() can only be called in main thread.

What it means

On wasm32, when a Window has fit_canvas_to_parent: true, the window-creation system in bevy_winit/src/system.rs calls winit_window.canvas() to restyle the HTML canvas. winit only permits that call from the browser's main thread; from any other thread it returns Err, which the .expect("window.canvas() can only be called in main thread.") converts into a panic during window creation.

Source

Thrown at crates/bevy_winit/src/system.rs:106

                commands.entity(entity).insert((
                    CachedWindow(window.clone()),
                    CachedCursorOptions(cursor_options.clone()),
                    WinitWindowPressedKeys::default(),
                ));

                if let Ok(handle_wrapper) = RawHandleWrapper::new(winit_window) {
                    commands.entity(entity).insert(handle_wrapper.clone());
                    if let Some(handle_holder) = handle_holder {
                        *handle_holder.0.lock().unwrap() = Some(handle_wrapper);
                    }
                }

                #[cfg(target_arch = "wasm32")]
                {
                    if window.fit_canvas_to_parent {
                        let canvas = winit_window
                            .canvas()
                            .expect("window.canvas() can only be called in main thread.");
                        let style = canvas.style();
                        style.set_property("width", "100%").unwrap();
                        style.set_property("height", "100%").unwrap();
                    }
                }

                #[cfg(target_os = "ios")]
                {
                    winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture);
                    winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture);
                    winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture);
                    if let Some((min, max)) = window.recognize_pan_gesture {
                        winit_window.recognize_pan_gesture(true, min, max);
                    } else {
                        winit_window.recognize_pan_gesture(false, 0, 0);
                    }
                }

View on GitHub (pinned to 396ca72708)

Solutions

  1. Keep the entire Bevy app — especially window creation — on the browser main thread; offload computation to tasks/workers instead of moving the app loop.
  2. Spawn windows reactively from systems via Commands so creation happens inside the main-thread schedule.
  3. Set fit_canvas_to_parent: false if canvas resizing must happen off the main thread (then apply CSS sizing yourself from JS).
  4. On wasm, drive the app with the standard wasm-bindgen main-thread entry point rather than a custom thread.

Example fix

// before: app run from a spawned thread on wasm
std::thread::spawn(|| App::new().add_plugins(DefaultPlugins).run());

// after: keep the app on the main thread; push heavy work onto task pools
fn main() { App::new().add_plugins(DefaultPlugins).add_systems(Update, heavy_work_async).run(); }
Defensive patterns

Strategy: validation

Validate before calling

// capture the main thread at startup and assert before creating windows on wasm
static MAIN_THREAD: std::sync::OnceLock<std::thread::ThreadId> = std::sync::OnceLock::new();

fn init_main_thread() { MAIN_THREAD.set(std::thread::current().id()).ok(); }

fn on_main_thread() -> bool {
    MAIN_THREAD.get().is_some_and(|id| *id == std::thread::current().id())
}

// before spawning a fit_canvas_to_parent window:
assert!(on_main_thread(), "window creation must happen on the main thread");

Try / catch

// Panic occurs inside the window-creation system; prevention is structural:
// keep the app loop on the browser main thread and use task pools for heavy work.

Prevention

When it happens

Trigger: Creating or respawn a Window with fit_canvas_to_parent = true from a web worker, a spawned std::thread, or JS-driven code not on the main thread; running the Bevy app loop off the main thread on the web target.

Common situations: Web deployments that move heavy work to workers and accidentally also drive window creation there; interop layers where JS triggers window creation from a callback running on a non-main thread; embedding Bevy in an existing JS app framework that manages threads.

Related errors


AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20). Data as JSON: /api/errors/ce42f24e2d2a2ea0. Report an issue: GitHub.