{"record":{"id":"095285ce00eed66c","repo":"emberjs/ember.js","slug":"attempted-to-unregister-a-destructor-with-an-objec","errorCode":null,"errorMessage":"Attempted to unregister a destructor with an object that is already destroying or destroyed","messagePattern":"Attempted to unregister a destructor with an object that is already destroying or destroyed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@glimmer/destroyable/index.ts","lineNumber":176,"sourceCode":"\n  let meta = getDestroyableMeta(destroyable);\n\n  let destructorsKey: 'eagerDestructors' | 'destructors' = eager\n    ? 'eagerDestructors'\n    : 'destructors';\n\n  meta[destructorsKey] = push(meta[destructorsKey], destructor);\n\n  return destructor;\n}\n\nexport function unregisterDestructor<T extends Destroyable>(\n  destroyable: T,\n  destructor: Destructor<T>,\n  eager = false\n): void {\n  if (DEBUG && isDestroying(destroyable)) {\n    throw new Error(\n      'Attempted to unregister a destructor with an object that is already destroying or destroyed'\n    );\n  }\n\n  let meta = getDestroyableMeta(destroyable);\n\n  let destructorsKey: 'eagerDestructors' | 'destructors' = eager\n    ? 'eagerDestructors'\n    : 'destructors';\n\n  meta[destructorsKey] = remove(\n    meta[destructorsKey],\n    destructor,\n    DEBUG && 'attempted to remove a destructor that was not registered with the destroyable'\n  );\n}\n\n////////////","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/emberjs/ember.js/blob/26f97246a8bf2e28edf26ac3093da2e86c04ffc5/packages/@glimmer/destroyable/index.ts#L158-L194","documentation":"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.","triggerScenarios":"Calling unregisterDestructor(obj, fn) after destroy(obj) began; teardown hooks racing with manual cleanup; double-teardown paths in components/helpers.","commonSituations":"Component willDestroy calling cleanup code that also unregisters destructors; reactive effects tearing down twice; tests destroying objects then calling teardown helpers again.","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"],"exampleFix":"// before\ndestroy(obj);\nunregisterDestructor(obj, fn);\n// after\nif (!isDestroying(obj) && !isDestroyed(obj)) {\n  unregisterDestructor(obj, fn);\n}","handlingStrategy":"validation","validationCode":"import { isDestroying, isDestroyed } from '@glimmer/destroyable';\nif (!isDestroying(obj) && !isDestroyed(obj)) {\n  unregisterDestructor(obj, fn);\n}","typeGuard":"function canUnregister(obj) { return !isDestroying(obj) && !isDestroyed(obj); }","tryCatchPattern":"try {\n  unregisterDestructor(obj, fn);\n} catch (e) {\n  if (String(e.message).includes('unregister a destructor')) {\n    // already tearing down; destruction will run the destructor anyway\n  } else { throw e; }\n}","preventionTips":["Make teardown idempotent with a disposed flag","Unregister before calling destroy, or skip unregistering entirely","Avoid duplicate cleanup paths (manual + willDestroy)"],"tags":["lifecycle","destructor","destroyable","glimmer","debug"],"backgroundTag":"register-destructor-after-destroy","analyzedSha":"26f97246a8bf2e28edf26ac3093da2e86c04ffc5","analyzedAt":"2026-09-01T05:01:28.182Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}