unity coroutines keep dying and i have no idea why

40 views 8 replies

okay so i'm losing my mind a little bit here. i have a coroutine that's supposed to handle a sequence of enemy attack animations — wait, play anim, wait, do damage, wait, return to idle. pretty basic stuff. it works fine maybe 70% of the time but sometimes it just... stops. no error, no warning, nothing in the console. the enemy just freezes mid-attack and stays there forever.

here's the coroutine:

IEnumerator AttackSequence(GameObject target)
{
    animator.SetTrigger("Attack");
    yield return new WaitForSeconds(attackWindup);

    if (target != null)
    {
        DealDamage(target);
    }

    yield return new WaitForSeconds(attackRecovery);
    animator.SetTrigger("Idle");
}

i start it with StartCoroutine(AttackSequence(target)) from an Update() check. my first thought was the target getting destroyed mid-coroutine, which is why i added the null check — but it's still happening even when the target is very much alive.

i read somewhere that coroutines die silently if the gameobject or monobehaviour gets disabled, but i'm not disabling anything manually. could something else be disabling the component? like maybe an animation event or something in the animator state machine?

also worth noting: i'm calling StopAllCoroutines() in a couple places during state transitions. could that be nuking the attack coroutine before it finishes? i assumed that was safe but now i'm second-guessing everything.

using Unity 2023.2 LTS btw. any help appreciated, tbh at this point i'd accept a workaround even if it's ugly lol.

coroutine deaths in Unity are almost always one of three things: the GameObject gets disabled, the MonoBehaviour gets destroyed, or you're yielding inside a try/catch and an exception is silently swallowing the iterator state.

the try/catch one is the sneaky one. this pattern looks fine but will silently kill the coroutine if anything throws inside the loop:

IEnumerator MyRoutine() {
    while (true) {
        try {
            DoSomethingRisky();
        } catch (Exception e) {
            Debug.LogError(e);
        }
        yield return null; // this line is OUTSIDE the try, so it's okay
    }
}

if the yield is inside the try block, Unity can't resume it after a throw. move all your yields outside try blocks and a lot of mysterious coroutine deaths just stop happening.

oh that's exactly what's happening to me. i have a yield inside a try because i was catching a web request timeout and thought wrapping the whole thing was cleaner. going to pull the yield out right now and see if that fixes the dropouts. thank you

coroutines dying silently is almost always one of three things: an unhandled exception inside the coroutine (Unity swallows these by default and just stops execution), the MonoBehaviour being destroyed mid-run, or — the sneaky one — calling StopAllCoroutines() somewhere upstream that you forgot about.

add this wrapper and it'll surface the hidden exceptions:

IEnumerator SafeCoroutine(IEnumerator inner) {
    while (true) {
        object current;
        try { if (!inner.MoveNext()) yield break;
              current = inner.Current; }
        catch (Exception e) { Debug.LogError($"Coroutine failed: {e}"); yield break; }
        yield return current;
    }
}

start everything through that and you'll see exactly what's killing them.

The StopAllCoroutines() call is almost certainly your problem. It kills every coroutine on that MonoBehaviour with no exceptions, including your attack sequence if a state transition fires mid-execution. Store the result of StartCoroutine() in a field and call StopCoroutine() on that specific reference instead. That way your transitions only stop what they actually need to stop and your attack sequence can finish cleanly.

Also worth checking: animator state-exit behaviours can disable components or send messages that kill coroutines indirectly. Burned me once and it took an embarrassingly long time to find.

the StopAllCoroutines() call is almost certainly your culprit. that nukes every coroutine on the MonoBehaviour with no exceptions — including the one that's mid-execution. if you're calling it during state transitions and those can happen while an attack is in flight, you've found your bug. fix is to track the coroutine with a reference and only stop the ones you actually mean to stop:

private Coroutine _attackCoroutine;

void StartAttack(GameObject target)
{
    if (_attackCoroutine != null)
        StopCoroutine(_attackCoroutine);
    _attackCoroutine = StartCoroutine(AttackSequence(target));
}

then your state transition code only calls StopCoroutine(_attackCoroutine) if it actually needs to interrupt an attack, rather than carpet-bombing everything.

slightly off-topic but the coroutine dying silently thing has burned me so many times. the StopAllCoroutines() in state transitions is almost certainly your problem — that call doesn't discriminate, it kills every coroutine running on that MonoBehaviour regardless of what started it or why. swap it out for StopCoroutine(attackCoroutineRef) where you store the return value of StartCoroutine() in a field. gives you surgical control instead of a sledgehammer.

coroutine spaghetti code meme

Replying to ShadowReed: coroutine deaths in Unity are almost always one of three things: the GameObject ...

the try/catch yield thing bit me too. worth noting that in Unity 2023+ you can use UniTask instead of coroutines and get proper async/await with real exception propagation. made the switch on my current project and haven't looked back. coroutines feel pretty ancient once you're used to actual async.

Replying to QuantumByte: the StopAllCoroutines() call is almost certainly your culprit. that nukes every ...

Late to the coroutines thread but wanted to add: if you're already on Unity 6, Awaitable is worth looking at as a drop-in replacement for coroutines in a lot of the common cases. Cancellation token support is built in rather than bolted on, and it composes better with async code you might be calling into from editor tooling. Still not a silver bullet but it cleaned up a lot of our uglier stop/restart coroutine patterns.