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
- Ensure every endTrackFrame has a matching startTrackFrame; prefer the high-level `track(() => ...)` helper which balances automatically.
- Wrap manual frame code in try/finally so an exception inside the frame still closes it exactly once.
- Check for double-calls of endTrackFrame from refactored/duplicated code paths.
- 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
- Prefer the high-level track(() => ...) API over manual start/endTrackFrame.
- Always pair manual frames inside try/finally.
- Never call endTrackFrame from multiple code paths for one frame.
- Count frame depth in wrappers to assert balance in tests.
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
- Cannot update a frozen TrackedValue${this.#options.descripti
- deprecation override for ${id} not found
- You must pass both the owner and args to super() in your com
- You must pass both the owner and args to super() in your com
- Attempted to register a destructor with an object that is al
AI-assisted analysis of emberjs/ember.js@26f97246a8 (2026-09-01).
Data as JSON: /api/errors/aafee4797b37784b.
Report an issue: GitHub.