{"record":{"id":"2bac752340bd740f","repo":"emberjs/ember.js","slug":"attempted-to-register-a-destructor-with-an-object","errorCode":null,"errorMessage":"Attempted to register a destructor with an object that is already destroying or destroyed","messagePattern":"Attempted to register 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":154,"sourceCode":"    );\n  }\n\n  let parentMeta = getDestroyableMeta(parent);\n  let childMeta = getDestroyableMeta(child);\n\n  parentMeta.children = push(parentMeta.children, child);\n  childMeta.parents = push(childMeta.parents, parent);\n\n  return child;\n}\n\nexport function registerDestructor<T extends Destroyable>(\n  destroyable: T,\n  destructor: Destructor<T>,\n  eager = false\n): Destructor<T> {\n  if (DEBUG && isDestroying(destroyable)) {\n    throw new Error(\n      'Attempted to register 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] = push(meta[destructorsKey], destructor);\n\n  return destructor;\n}\n\nexport function unregisterDestructor<T extends Destroyable>(\n  destroyable: T,\n  destructor: Destructor<T>,","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/emberjs/ember.js/blob/26f97246a8bf2e28edf26ac3093da2e86c04ffc5/packages/@glimmer/destroyable/index.ts#L136-L172","documentation":"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.","triggerScenarios":"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.","commonSituations":"Lazily registering destructors inside getters or effects that run during teardown; tests that destroy fixtures then reuse them; framework internals associating resources during shutdown.","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"],"exampleFix":"// before\nif (!obj.resource) {\n  obj.resource = acquire();\n  registerDestructor(obj, () => obj.resource.release());\n}\n// after\nregisterDestructor(obj, () => obj.resource && obj.resource.release());\nobj.resource = obj.resource || acquire();","handlingStrategy":"validation","validationCode":"import { isDestroying, isDestroyed } from '@glimmer/destroyable';\nif (!isDestroying(obj) && !isDestroyed(obj)) {\n  registerDestructor(obj, cleanup);\n}","typeGuard":"function canRegister(obj) { return !isDestroying(obj) && !isDestroyed(obj); }","tryCatchPattern":"try {\n  registerDestructor(obj, cleanup);\n} catch (e) {\n  if (String(e.message).includes('register a destructor')) {\n    // object is tearing down; run cleanup immediately instead\n    cleanup(obj);\n  } else { throw e; }\n}","preventionTips":["Register destructors immediately after creating the destroyable","Check isDestroying/isDestroyed before lifecycle mutations","Never register destructors from within other destructors on the same object"],"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"}