emberjs/ember.js · error · Error

attempted to close a tracking frame, but one was not open

Error message

attempted to close a tracking frame, but one was not open

What it means

endTrackFrame pops the tracking frame stack; in DEBUG builds it throws if the stack is empty, meaning endTrackFrame was called without a matching startTrackFrame. This protects the tracker state machine (CURRENT_TRACKER / OPEN_TRACK_FRAMES) from becoming corrupted.

Source

Thrown at packages/@glimmer/validator/lib/tracking.ts:74

const OPEN_TRACK_FRAMES: (Tracker | null)[] = [];

export function beginTrackFrame(debuggingContext?: string | false): void {
  OPEN_TRACK_FRAMES.push(CURRENT_TRACKER);

  CURRENT_TRACKER = new Tracker();

  if (DEBUG) {
    unwrap(debug.beginTrackingTransaction)(debuggingContext);
  }
}

export function endTrackFrame(): Tag {
  let current = CURRENT_TRACKER;

  if (DEBUG) {
    if (OPEN_TRACK_FRAMES.length === 0) {
      throw new Error('attempted to close a tracking frame, but one was not open');
    }

    unwrap(debug.endTrackingTransaction)();
  }

  CURRENT_TRACKER = OPEN_TRACK_FRAMES.pop() || null;

  return unwrap(current).combine();
}

export function beginUntrackFrame(): void {
  OPEN_TRACK_FRAMES.push(CURRENT_TRACKER);
  CURRENT_TRACKER = null;
}

export function endUntrackFrame(): void {
  if (DEBUG && OPEN_TRACK_FRAMES.length === 0) {
    throw new Error('attempted to close a tracking frame, but one was not open');

View on GitHub (pinned to 26f97246a8)

Solutions

  1. Ensure every endTrackFrame has a matching startTrackFrame; prefer the high-level `track(() => ...)` helper which balances automatically.
  2. Wrap manual frame code in try/finally so an exception inside the frame still closes it exactly once.
  3. Check for double-calls of endTrackFrame from refactored/duplicated code paths.
  4. If seen inside a library update, report it — it usually indicates a framework bug in tag lifecycle handling.

Example fix

// before
startTrackFrame();
let tag = computeTag(); // may throw
endTrackFrame();

// after
let tag = track(() => computeTag()); // balanced by construction
Defensive patterns

Strategy: try-catch

Validate before calling

function trackedFrameOpen() {
  // expose from validator internals or track locally in your wrapper
  return frameOpenCount > 0;
}
if (!trackedFrameOpen()) throw new Error('startTrackFrame must be called before endTrackFrame');

Try / catch

startTrackFrame();
try {
  const tag = computeTag();
  return tag;
} finally {
  endTrackFrame(); // closes exactly once, even on throw
}

Prevention

When it happens

Trigger: Calling `endTrackFrame()` directly, or an unbalanced internal path from callers like `track`, `tag`, `getValue`, `update`, `argsTag`, or `combined` that closes a frame when none was opened (e.g. a tag/GETTER used after the tracking transaction already ended).

Common situations: Manual use of low-level @glimmer/validator APIs (track/startTrackFrame) with mismatched begin/end; re-entrant tag evaluation during a `cached` getter; exception in a tracked computation that skipped the matching end call leaving state inconsistent for the next call.

Related errors


AI-assisted analysis of emberjs/ember.js@26f97246a8 (2026-09-01). Data as JSON: /api/errors/aafee4797b37784b. Report an issue: GitHub.