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

  1. Unregister destructors before initiating destroy(), or don't unregister at all (destroy runs them)
  2. Guard calls with isDestroying(obj) || isDestroyed(obj)
  3. Remove duplicate teardown paths so cleanup runs exactly once
  4. 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

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


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