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
- Keep the entire Bevy app — especially window creation — on the browser main thread; offload computation to tasks/workers instead of moving the app loop.
- Spawn windows reactively from systems via Commands so creation happens inside the main-thread schedule.
- Set fit_canvas_to_parent: false if canvas resizing must happen off the main thread (then apply CSS sizing yourself from JS).
- 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
- On wasm, never run the Bevy app from a worker or spawned thread.
- Create windows via Commands from systems so creation stays on the main-thread schedule.
- If you must style the canvas off-thread, do it from JS and set fit_canvas_to_parent: false.
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
- Cannot query for canvas element.
- Cannot find element: {selector}.
- Failed to build event loop
- Attempted to insert invalid value of type {}.
- Attempted to push invalid value of type {}.
AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20).
Data as JSON: /api/errors/ce42f24e2d2a2ea0.
Report an issue: GitHub.