Skip to content

Uncaught TypeError: Cannot read properties of null (reading 'cancelAnimationFrame') on session end #5844

Open

Description

@XRDeltas

Description

When an A-Frame application ends an XR session and immediately re-enters, an Uncaught TypeError: Cannot read properties of null (reading 'cancelAnimationFrame') is thrown. This intermittently crashes the application during XR session lifecycle transitions.

The root cause is in A-Frame's render loop: cancelAnimationFrame is called with a null handle when the animation frame ID is cleaned up before the stop function reads it.

Environment

  • A-Frame version: Latest (affects A-Painter, A-Blast, and other A-Frame WebXR apps)
  • Browser: Chromium 124+ (mobile emulation triggers more frequently)
  • XR Mode: WebXR immersive-vr

Steps to Reproduce

  1. Open an A-Frame WebXR app (e.g., A-Painter)
  2. Enter VR (navigator.xr.requestSession('immersive-vr'))
  3. Perform interactions (select, aim)
  4. Exit VR (end the session)
  5. Re-enter VR immediately
  6. Repeat steps 4-5 multiple times

Expected Behavior

cancelAnimationFrame should gracefully handle a null handle, or the animation frame handle should never be null during the render loop.

Actual Behavior

Uncaught TypeError: Cannot read properties of null (reading 'cancelAnimationFrame') at stopRendering (aframe.js:XXXX)

Observed in ~15% of replay traces (3 out of 19 traces for A-Painter on mobile Chromium).

Root Cause

In A-Frame's render loop, the cancelAnimationFrame call does not guard against null:

let animFrameId = null;

function startRendering() {
    animFrameId = requestAnimationFrame(render);
}

function stopRendering() {
    cancelAnimationFrame(animFrameId);  // ← animFrameId can be null
    animFrameId = null;
}
Under rapid XR session switching, a race condition exists where animFrameId is set to null before stopRendering reads it.

Proposed Fix
Add a null guard:
function stopRendering() {
    if (animFrameId != null) {
        cancelAnimationFrame(animFrameId);
    }
    animFrameId = null;
}
Additional Context
This bug was discovered via an automated cross-host replay system (WebXRBench) that replays WebXR interaction traces across different browser configurations. The error was captured in 192 divergence instances across 17 traces and 2 A-Frame apps.

Severity: Medium — intermittent but causes JavaScript execution interruption that can manifest as rendering glitches or state corruption.

Activity

  1. mrxz commented on Jun 30, 2026

    @mrxz
    Contributor

    Similarly to #5845 there are quite a few factual inaccuracies in this report.

    The code in the "Root Cause" section seems to confuse Window.cancelAnimationFrame and XRSession.cancelAnimationFrame. Regardless, it's completely fine to pass a null animation frame callback handle to either. The actual error message indicates that the underlying object was null at the time of the call. It's unclear to me how this could happen, as A-Frame does not call cancelAnimationFrame itself, instead delegating it to Three.js. But ultimately the context is either the window or an XRSession instance during an immersive session, it should never be null.

    Please provide the actual stack trace and, if possible, a minimal reproduction. The reproduction steps listed imply manually calling navigator.xr.requestSession('immersive-vr') which isn't the proper way to enter VR in A-Frame.

    @XRDeltas Pure speculation from my part, but could it be that your framework creates a proxy XRSession that forwards calls to the actual session? If so, please ensure the session remains spec-compliant after the immersive session has ended. Whenever reporting bugs, please make sure they reproduce without your framework in the mix as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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