DioxusLabs/dioxus · error

access to `window`

Error message

access to `window`

What it means

`WebHistory::new(prefix, do_scroll_restoration)` - the browser History implementation behind the Dioxus router - immediately runs `window().expect("access to `window`")` in `new_inner`. Without a Window global (web worker, server/SSR process, headless test runtime), construction panics before routing ever starts.

Source

Thrown at packages/web/src/history.rs:48

    /// Create a new [`WebHistory`].
    ///
    /// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
    /// state. It'll also set the browsers scroll restoration to `manual`.
    pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
        let myself = Self::new_inner(prefix, do_scroll_restoration);

        let current_route = dioxus_history::History::current_route(&myself);
        let current_route_str = current_route.to_string();
        let prefix_str = myself.prefix.as_deref().unwrap_or("");
        let current_url = format!("{prefix_str}{current_route_str}");
        let state = myself.create_state();
        let _ = replace_state_with_url(&myself.history, &state, Some(&current_url));

        myself
    }

    fn new_inner(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
        let window = window().expect("access to `window`");
        let history = window.history().expect("`window` has access to `history`");

        if do_scroll_restoration {
            history
                .set_scroll_restoration(ScrollRestoration::Manual)
                .expect("`history` can set scroll restoration");
        }

        let prefix = prefix
            // If there isn't a base path, try to grab one from the CLI
            .or_else(dioxus_cli_config::web_base_path)
            // Normalize the prefix to start and end with no slashes
            .as_ref()
            .map(|prefix| prefix.trim_matches('/'))
            // If the prefix is empty, don't add it
            .filter(|prefix| !prefix.is_empty())
            // Otherwise, start with a slash
            .map(|prefix| format!("/{prefix}"));

View on GitHub (pinned to 393d190a80)

Solutions

  1. Construct WebHistory only in browser builds (`#[cfg(target_arch = "wasm32")]`)
  2. On the server or in tests, provide a MemoryHistory (`Rc<dyn History>`) instead - this is what dioxus fullstack/desktop renderers do
  3. Check `web_sys::window().is_some()` before choosing the history implementation

Example fix

// before: constructed unconditionally, panics on server/worker
let history = Rc::new(WebHistory::new(None, true)) as Rc<dyn History>;
// after: pick per environment
let history = if web_sys::window().is_some() {
    Rc::new(WebHistory::new(None, true)) as Rc<dyn History>
} else {
    Rc::new(MemoryHistory::default()) as Rc<dyn History>
};
Defensive patterns

Strategy: type-guard

Validate before calling

// choose the history implementation before constructing
if web_sys::window().is_none() {
    // not a browser main thread: do not construct WebHistory here
    let history: Rc<dyn History> = Rc::new(MemoryHistory::default());
    // ... provide it to the router and return
}

Type guard

fn can_use_web_history() -> bool {
    web_sys::window().is_some()
}

Prevention

When it happens

Trigger: Constructing WebHistory outside the browser main thread; a fullstack server build that instantiates web history because the router integration was not cfg-gated; wasm-bindgen-test creating a router outside a page.

Common situations: Shared routing setup code compiled into both server and client targets; running the app inside a worker; integration tests exercising navigation logic headlessly.

Related errors


AI-assisted analysis of DioxusLabs/dioxus@393d190a80 (2026-08-16). Data as JSON: /api/errors/51d3b320885f3eee. Report an issue: GitHub.