emberjs/ember.js · error · Error

Attempted to register a destructor with an object that is al

Error message

Attempted to register a destructor with an object that is already destroying or destroyed

What it means

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.

Source

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

    );
  }

  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> {
  if (DEBUG && isDestroying(destroyable)) {
    throw new Error(
      'Attempted to register a destructor with an object that is already destroying or destroyed'
    );
  }

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

View on GitHub (pinned to 26f97246a8)

Solutions

  1. Register all destructors at object creation time, before any teardown begins
  2. Guard with isDestroying(obj)/isDestroyed(obj) before registering
  3. Restructure so late-acquired resources are associated with a still-live parent
  4. Fix test fixtures to create fresh destroyables instead of reusing destroyed ones

Example fix

// before
if (!obj.resource) {
  obj.resource = acquire();
  registerDestructor(obj, () => obj.resource.release());
}
// after
registerDestructor(obj, () => obj.resource && obj.resource.release());
obj.resource = obj.resource || acquire();
Defensive patterns

Strategy: validation

Validate before calling

import { isDestroying, isDestroyed } from '@glimmer/destroyable';
if (!isDestroying(obj) && !isDestroyed(obj)) {
  registerDestructor(obj, cleanup);
}

Type guard

function canRegister(obj) { return !isDestroying(obj) && !isDestroyed(obj); }

Try / catch

try {
  registerDestructor(obj, cleanup);
} catch (e) {
  if (String(e.message).includes('register a destructor')) {
    // object is tearing down; run cleanup immediately instead
    cleanup(obj);
  } else { throw e; }
}

Prevention

When it happens

Trigger: 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.

Common situations: Lazily registering destructors inside getters or effects that run during teardown; tests that destroy fixtures then reuse them; framework internals associating resources during shutdown.

Related errors


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