linebender/druid · error

Failed to call setTimeout with a callback

Error message

Failed to call setTimeout with a callback

What it means

Panic from `.expect()` when `set_timeout_with_callback_and_timeout_and_arguments_0` — the wasm-bindgen binding for JS `setTimeout` — throws inside `Window::request_timer`. The browser rejects `setTimeout` calls from a context that is being destroyed or in restricted environments, and since druid-shell's timer tokens rely on the callback being scheduled, failure is fatal.

Solutions

  1. Check the page is still alive before requesting timers; don't request timers from event handlers racing window close/navigation
  2. Validate the timeout value is a finite, non-negative number (NaN/undefined throws in the JS binding)
  3. Supply complete timer polyfills when running under Node/jsdom test harnesses
  4. Inspect the browser console for the underlying JS exception and fix the page lifecycle problem

Example fix

// before
let token = sink.request_timer(Duration::from_secs(1), None); // panics after window closed
// after: only request timers while the window is still live
if !window_closing.load(Ordering::SeqCst) {
    let token = sink.request_timer(Duration::from_secs(1), None);
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate the timer interval before requesting it
fn valid_interval(d: std::time::Duration) -> bool {
    d.as_millis() <= i32::MAX as u128
}

Type guard

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

Try / catch

// Guard timer requests at the call site:
if page_alive() && valid_interval(interval) {
    sink.request_timer(interval, None);
} else {
    log::warn!("skipping timer request: page dead or invalid interval");
}

Prevention

When it happens

Trigger: Calling `ExtEventSink::request_timer` / `Window::request_timer` (web backend) while the page is unloading, from a context where `setTimeout` is unavailable (worker, jsdom without timers), or with an `interval` the browser rejects (e.g. non-finite value).

Common situations: Scheduling a timer from an event sink after window close was requested, running wasm tests under Node with incomplete DOM timer shims, or very large/NaN timeout values reaching the JS layer.

Understand the failure class

Related errors


AI-assisted analysis of linebender/druid@0f8b1195e4 (2026-09-10). Data as JSON: /api/errors/efb20fdea0cd2048. Report an issue: GitHub.

Appendix: source

Thrown at druid-shell/src/backend/web/window.rs:641

            }
        };

        let token = TimerToken::next();

        if let Some(state) = self.0.upgrade() {
            let s = state.clone();
            let f = move || {
                if let Ok(mut handler_borrow) = s.handler.try_borrow_mut() {
                    handler_borrow.timer(token);
                }
            };
            state
                .window
                .set_timeout_with_callback_and_timeout_and_arguments_0(
                    Closure::once_into_js(f).as_ref().unchecked_ref(),
                    interval,
                )
                .expect("Failed to call setTimeout with a callback");
        }
        token
    }

    pub fn set_cursor(&mut self, cursor: &Cursor) {
        if let Some(s) = self.0.upgrade() {
            set_cursor(&s.canvas, cursor);
        }
    }

    pub fn make_cursor(&self, _cursor_desc: &CursorDesc) -> Option<Cursor> {
        warn!("Custom cursors are not yet supported in the web backend");
        None
    }

    pub fn open_file(&mut self, _options: FileDialogOptions) -> Option<FileDialogToken> {
        warn!("open_file is currently unimplemented for web.");
        None

View on GitHub (pinned to 0f8b1195e4)