emberjs/ember.js · error · Error
Attempted to unregister a destructor with an object that is
Error message
Attempted to unregister a destructor with an object that is already destroying or destroyed
What it means
unregisterDestructor removes a previously registered destructor. Throwing in DEBUG when the object is already destroying/destroyed prevents mutating the destructor list mid-teardown, which could skip or corrupt cleanup ordering.
Source
Thrown at packages/@glimmer/destroyable/index.ts:176
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>,
eager = false
): void {
if (DEBUG && isDestroying(destroyable)) {
throw new Error(
'Attempted to unregister a destructor with an object that is already destroying or destroyed'
);
}
let meta = getDestroyableMeta(destroyable);
let destructorsKey: 'eagerDestructors' | 'destructors' = eager
? 'eagerDestructors'
: 'destructors';
meta[destructorsKey] = remove(
meta[destructorsKey],
destructor,
DEBUG && 'attempted to remove a destructor that was not registered with the destroyable'
);
}
////////////View on GitHub (pinned to 26f97246a8)
Solutions
- Unregister destructors before initiating destroy(), or don't unregister at all (destroy runs them)
- Guard calls with isDestroying(obj) || isDestroyed(obj)
- Remove duplicate teardown paths so cleanup runs exactly once
- Use a disposed flag to make teardown idempotent
Example fix
// before
destroy(obj);
unregisterDestructor(obj, fn);
// after
if (!isDestroying(obj) && !isDestroyed(obj)) {
unregisterDestructor(obj, fn);
} Defensive patterns
Strategy: validation
Validate before calling
import { isDestroying, isDestroyed } from '@glimmer/destroyable';
if (!isDestroying(obj) && !isDestroyed(obj)) {
unregisterDestructor(obj, fn);
} Type guard
function canUnregister(obj) { return !isDestroying(obj) && !isDestroyed(obj); } Try / catch
try {
unregisterDestructor(obj, fn);
} catch (e) {
if (String(e.message).includes('unregister a destructor')) {
// already tearing down; destruction will run the destructor anyway
} else { throw e; }
} Prevention
- Make teardown idempotent with a disposed flag
- Unregister before calling destroy, or skip unregistering entirely
- Avoid duplicate cleanup paths (manual + willDestroy)
When it happens
Trigger: Calling unregisterDestructor(obj, fn) after destroy(obj) began; teardown hooks racing with manual cleanup; double-teardown paths in components/helpers.
Common situations: Component willDestroy calling cleanup code that also unregisters destructors; reactive effects tearing down twice; tests destroying objects then calling teardown helpers again.
Related errors
- Attempted to register a destructor with an object that is al
- 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/095285ce00eed66c.
Report an issue: GitHub.