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.