Describe the Bug
TimeoutFuture documents that it "will never resolve to Err" and encodes that belief in poll by calling .unwrap_throw(). This invariant does not hold. The receiver can legitimately yield Err(oneshot::Canceled), at which point unwrap_throw() throws.
Error: called `Result::unwrap_throw()` on an `Err` value
at __wbindgen_throw
at wasm_bindgen::throw_str
at <gloo_timers::future::TimeoutFuture as core::future::future::Future>::poll
Steps to Reproduce
Under wasm-bindgen --weak-refs, the cleanup of resources can happen in different stages. With weak refs enabled, JS-side closures are reclaimed by the JS engine's GC rather than freed synchronously when the Rust side drops the Timeout.
- Rust drops a
TimeoutFuture (e.g. a struct prop holding the timer is overwritten with a new timer).
clearTimeout is called for the old timer.
- The Rust
Timeout struct is dropped, but the Closure holding tx is not freed immediately — that's deferred to the next GC pass.
- Before the GC runs, the new
TimeoutFuture is polled (advancing the async state machine), and the old rx is polled one final time.
- When the GC eventually finalizes the closure,
tx is dropped without send() having been called. But by the time the GC runs, the rx may still be accessible and may get polled again.
Reference to tracking issue: nimiq/core-rs-albatross#3844
Expected Behavior
Don't work with the assumption the rx cannot fail when polled and handle the invariants properly.
Actual Behavior
An Err() is returned which is being unwrapped into a panic.
Describe the Bug
TimeoutFuturedocuments that it "will never resolve toErr" and encodes that belief inpollby calling.unwrap_throw(). This invariant does not hold. The receiver can legitimately yieldErr(oneshot::Canceled), at which pointunwrap_throw()throws.Steps to Reproduce
Under
wasm-bindgen --weak-refs, the cleanup of resources can happen in different stages. With weak refs enabled, JS-side closures are reclaimed by the JS engine's GC rather than freed synchronously when the Rust side drops theTimeout.TimeoutFuture(e.g. a struct prop holding the timer is overwritten with a new timer).clearTimeoutis called for the old timer.Timeoutstruct is dropped, but theClosureholdingtxis not freed immediately — that's deferred to the next GC pass.TimeoutFutureis polled (advancing the async state machine), and the oldrxis polled one final time.txis dropped withoutsend()having been called. But by the time the GC runs, therxmay still be accessible and may get polled again.Reference to tracking issue: nimiq/core-rs-albatross#3844
Expected Behavior
Don't work with the assumption the
rxcannot fail when polled and handle the invariants properly.Actual Behavior
An
Err()is returned which is being unwrapped into a panic.