emberjs/ember.js · error · Error

Attempted to associate a destroyable child with an object th

Error message

Attempted to associate a destroyable child with an object that is already destroying or destroyed

What it means

associateDestroyableChild links a child destroyable's lifetime to a parent so the child is destroyed with the parent. In DEBUG builds, doing this on a parent that is already destroying/destroyed is invalid — the child could never be cleaned up through the parent — so it throws.

Source

Thrown at packages/@glimmer/destroyable/index.ts:134

      DESTROYABLE_META.set(destroyable, meta as unknown as DestroyableMeta<Destroyable>);
    }

    return meta;
  }

  let meta = DESTROYABLE_META.get(destroyable);

  if (meta === undefined) {
    meta = createMeta(destroyable) as unknown as DestroyableMeta<Destroyable>;
    DESTROYABLE_META.set(destroyable, meta);
  }

  return meta as unknown as DestroyableMeta<T>;
}

export function associateDestroyableChild<T extends Destroyable>(parent: Destroyable, child: T): T {
  if (DEBUG && isDestroying(parent)) {
    throw new Error(
      'Attempted to associate a destroyable child with an object that is already destroying or destroyed'
    );
  }

  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> {

View on GitHub (pinned to 26f97246a8)

Solutions

  1. Associate children at creation time, before any teardown can begin
  2. Check isDestroying(parent)/isDestroyed(parent) before associating and dispose the child independently otherwise
  3. Cancel async work on teardown (use a token or the parent's destructor) so late resources aren't attached
  4. Restructure so the child is owned by a longer-lived, still-active parent

Example fix

// before
parent.resources.push(acquire());
associateDestroyableChild(parent, child);
// after
if (isDestroying(parent) || isDestroyed(parent)) {
  destroy(child);
} else {
  associateDestroyableChild(parent, child);
}
Defensive patterns

Strategy: validation

Validate before calling

import { isDestroying, isDestroyed } from '@glimmer/destroyable';
if (isDestroying(parent) || isDestroyed(parent)) {
  destroy(child); // dispose independently
} else {
  associateDestroyableChild(parent, child);
}

Type guard

function canAssociate(parent) { return !isDestroying(parent) && !isDestroyed(parent); }

Try / catch

try {
  associateDestroyableChild(parent, child);
} catch (e) {
  if (String(e.message).includes('associate a destroyable child')) {
    destroy(child); // parent is gone; clean up child directly
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling associateDestroyableChild(parent, child) after destroy(parent) started; creating resources inside willDestroy; async work resolving after teardown began and then associating its resources with the dead parent.

Common situations: Promise callbacks attaching resources post-teardown; renderComponent/helpers creating destroyables during shutdown; tests with leaked async teardown ordering.

Related errors


AI-assisted analysis of emberjs/ember.js@26f97246a8 (2026-09-01). Data as JSON: /api/errors/1f346deb446ac8a9. Report an issue: GitHub.