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
- Check the page is still alive before requesting timers; don't request timers from event handlers racing window close/navigation
- Validate the timeout value is a finite, non-negative number (NaN/undefined throws in the JS binding)
- Supply complete timer polyfills when running under Node/jsdom test harnesses
- 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
- Never request timers from code racing window close or navigation
- Clamp durations to the browser's safe setTimeout range (< 2^31 ms)
- Stop async tasks when the window closes so they can't schedule timers
- Polyfill setTimeout fully in test harnesses
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
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.");
NoneView on GitHub (pinned to 0f8b1195e4)