Skip to content

use_animated_open: unhandled promise rejection (AbortError) when a CSS transition/animation is canceled by DOM removal #290

Description

@ziimakc

Summary

use_animated_open (in primitives/src/lib.rs) waits for in-flight CSS animations/transitions to finish before removing a closed element from the DOM, via:

const id = await dioxus.recv();
const element = document.getElementById(id);
if (element && element.getAnimations().length > 0) {
    Promise.all(element.getAnimations().map((animation) => animation.finished)).then(() => {
        dioxus.send(true);
    });
} else {
    dioxus.send(true);
}

If the element (or its animations) gets removed/canceled before the animation finishes naturally, each canceled animation's .finished promise rejects with a DOMException (AbortError: The user aborted a request.) instead of resolving. Since Promise.all(...).then(...) has no .catch(), this surfaces as an uncaught, unhandled promise rejection in the browser console:

Uncaught (in promise) AbortError: The user aborted a request.
    at eval (eval:8)
    at eval (eval:16)

Repro

Any DropdownMenu (or other component built on use_animated_open) where the content element has a CSS transition (not even a keyframe animation — a plain transition: opacity 0.2s ease, transform 0.2s ease is enough, since CSS Transitions are also returned by Element.getAnimations()) will hit this if the element is unmounted/removed from the DOM while the close transition is still in flight — e.g. navigating away via a DropdownMenuItem's on_select handler immediately after the menu closes.

Confirmed via Chrome DevTools Protocol (Runtime.exceptionThrown + Debugger.getScriptSource) that the exception originates exactly at the animation.finished access inside this eval script.

Suggested fix

Add a .catch() (or per-animation .catch(() => {})) around the Promise.all(...) so a canceled animation doesn't produce an unhandled rejection, e.g.:

Promise.all(element.getAnimations().map((animation) => animation.finished.catch(() => {}))).then(() => {
    dioxus.send(true);
});

Workaround (in our app)

We worked around it at the CSS layer by setting animation: none !important; transition: none !important; on the dropdown content elements we control, which prevents getAnimations() from ever returning anything and avoids the Promise.all branch entirely. This isn't a real fix for consumers who rely on the open/close animation, though.

Environment

  • dioxus / dioxus-primitives 0.7.9 (git dependency, commit bf007c1)
  • Chrome (Playwright-driven, headless)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions