emberjs/ember.js · error · Error
Attempted to register a destructor with an object that is al
Error message
Attempted to register a destructor with an object that is already destroying or destroyed
What it means
registerDestructor attaches a cleanup callback to a destroyable object. In DEBUG builds, registering a destructor on an object already in isDestroying state is invalid because the destruction phase has begun and the new destructor may never run correctly, so it throws.
Source
Thrown at packages/@glimmer/destroyable/index.ts:154
);
}
let parentMeta = getDestroyableMeta(parent);
let childMeta = getDestroyableMeta(child);
parentMeta.children = push(parentMeta.children, child);
childMeta.parents = push(childMeta.parents, parent);
return child;
}
export function registerDestructor<T extends Destroyable>(
destroyable: T,
destructor: Destructor<T>,
eager = false
): Destructor<T> {
if (DEBUG && isDestroying(destroyable)) {
throw new Error(
'Attempted to register a destructor with an object that is already destroying or destroyed'
);
}
let meta = getDestroyableMeta(destroyable);
let destructorsKey: 'eagerDestructors' | 'destructors' = eager
? 'eagerDestructors'
: 'destructors';
meta[destructorsKey] = push(meta[destructorsKey], destructor);
return destructor;
}
export function unregisterDestructor<T extends Destroyable>(
destroyable: T,
destructor: Destructor<T>,View on GitHub (pinned to 26f97246a8)
Solutions
- Register all destructors at object creation time, before any teardown begins
- Guard with isDestroying(obj)/isDestroyed(obj) before registering
- Restructure so late-acquired resources are associated with a still-live parent
- Fix test fixtures to create fresh destroyables instead of reusing destroyed ones
Example fix
// before
if (!obj.resource) {
obj.resource = acquire();
registerDestructor(obj, () => obj.resource.release());
}
// after
registerDestructor(obj, () => obj.resource && obj.resource.release());
obj.resource = obj.resource || acquire(); Defensive patterns
Strategy: validation
Validate before calling
import { isDestroying, isDestroyed } from '@glimmer/destroyable';
if (!isDestroying(obj) && !isDestroyed(obj)) {
registerDestructor(obj, cleanup);
} Type guard
function canRegister(obj) { return !isDestroying(obj) && !isDestroyed(obj); } Try / catch
try {
registerDestructor(obj, cleanup);
} catch (e) {
if (String(e.message).includes('register a destructor')) {
// object is tearing down; run cleanup immediately instead
cleanup(obj);
} else { throw e; }
} Prevention
- Register destructors immediately after creating the destroyable
- Check isDestroying/isDestroyed before lifecycle mutations
- Never register destructors from within other destructors on the same object
When it happens
Trigger: Calling registerDestructor(obj, fn) after destroy(obj) started or the object's parent began destroying it; registering during a destructor callback of the same object; lifecycle mis-ordering in tests.
Common situations: Lazily registering destructors inside getters or effects that run during teardown; tests that destroy fixtures then reuse them; framework internals associating resources during shutdown.
Related errors
- Attempted to unregister a destructor with an object that is
- Attempted to associate a destroyable child with an object th
- 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
AI-assisted analysis of emberjs/ember.js@26f97246a8 (2026-09-01).
Data as JSON: /api/errors/2bac752340bd740f.
Report an issue: GitHub.