Quick answer: A coroutine is owned by the MonoBehaviour that started it. If you started it on a manager or a still-alive object, it keeps running after your object dies. Start on self, and stop in OnDestroy.

An enemy’s “attack after delay” coroutine still fires — spawning a projectile — seconds after the enemy was destroyed.

Coroutines Belong to Their Starter

StartCoroutine ties the coroutine to the MonoBehaviour you called it on. Call it on a long-lived GameManager and the routine survives your enemy’s death — still executing its body against now-invalid state.

Start on Self

// on the enemy MonoBehaviour
StartCoroutine(AttackAfterDelay());

Coroutines started on this are automatically stopped when the GameObject is destroyed or the component is disabled. That alone fixes most leaks.

Stop Explicitly in OnDestroy

private Coroutine _attackRoutine;

void StartAttack() {
    _attackRoutine = StartCoroutine(AttackAfterDelay());
}

void OnDestroy() {
    if (_attackRoutine != null) StopCoroutine(_attackRoutine);
}

Store the handle and stop it on destroy — belt and suspenders, and required if you ever started it elsewhere.

Guard the Body Anyway

After every yield, the object may have been destroyed mid-wait. A quick if (this == null) yield break; after long yields is cheap insurance.

Verifying

Destroy the enemy mid-windup. No delayed projectile spawns. No NullReference from the routine touching a dead object.

“Coroutines die with the object that started them — so start them on self, and stop them on destroy.”

Never start gameplay coroutines on a global manager ‘for convenience’ — that’s exactly how they outlive their owners.