StackOverflowException when trying starting a coroutine

Hello, I’m trying to use a coroutine after that same coroutine ends, but it doesn’t work and throws the exception mentioned above. It is possible to do this? Here’s my code:

private IEnumerator FillBottle() 
{
    //Doing some logic here
    if (milkFill.MilkLevel == 1) 
    {
        yield return new WaitForSeconds(3);
    }

    while (isOpen && (milkFill.MilkLevel != milkFill.MaxMilkLevel))
    {
        yield return new WaitForSeconds(0.4f);            
        milkFill.FillMilk();
    }
    //Then reuse the coroutine
    yield return StartCoroutine(FillBottle());
    
    //This cause: StackOverflowException: The requested operation caused a stack overflow.
}

Did you hear about recursion with no end?
You got it with expected exception.

For years using Coroutines, I came with the next method to start it safely when you have only 1 Coroutine running at the same time on the same Game Object:

private Coroutine showCommercialBreakCoroutineReference;

public void StartShowingCommercialBreak()
{
    if (showCommercialBreakCoroutineReference != null)
    {
        StopCoroutine(showCommercialBreakCoroutineReference);

        showCommercialBreakCoroutineReference = null;
    }

    showCommercialBreakCoroutineReference = StartCoroutine(
            ShowCommercialBreakCoroutine());
}

private IEnumerator ShowCommercialBreakCoroutine()
{
    yield return null;
}

Additionally to this, if you observe any unexpected behavior when you have Several Coroutines running on the same Game Object, you can stop other coroutines (which were not stopped directly via your code but should have been stopped logically) on this Game Object before running a new one.

Example:

private void StopAllCustomCoroutinesOfShopPanel()
{
    StopShowingRewardedVideo();

    StopPurchasingPro();

    StopRestoringTransactions();
}

public void StartPurchasingPro()
{
    StopAllCustomCoroutinesOfShopPanel();

    purchaseProCoroutineReference =
        StartCoroutine(PurchaseProCoroutine());
}

private void StopPurchasingPro()
{
    if (purchaseProCoroutineReference != null)
    {
        StopCoroutine(purchaseProCoroutineReference);

        purchaseProCoroutineReference = null;
    }
}

I tried using this approach, but I got the same exception. Maybe I’m implementing it the wrong way?

private Coroutine fillBottleReference;

    public void OpenFaucet()
    {
        /*  Unrelated Logic
        isOpen = true;
        if (firstAttempt != true)
        {
            SystemsCheck.Instance.RemoveClosedSystem();
        }
        firstAttempt = false;
        StartCoroutine(faucetAnimations.OpenFaucet());
        */

        if (fillBottleReference != null)
        {
            StopCoroutine(fillBottleReference);
            fillBottleReference = null;
        }

        fillBottleReference = StartCoroutine(FillBottle());
    }

private IEnumerator FillBottle() 
{
    if (milkFill.MilkLevel == 1) 
    {
        yield return new WaitForSeconds(3);
    }

    while (isOpen && (milkFill.MilkLevel != milkFill.MaxMilkLevel))
    {
        yield return new WaitForSeconds(0.4f);            
        milkFill.FillMilk();
    }

    OpenFaucet();
    yield return null;
}

Edit: What I’m trying to do with this code is filling a bottle with milk in a loop. When it cap its max capacity the bottle fills again from 0, and that’s why I need that Coroutine to run again.

This call is still inside Coroutine which contains StartCoroutine() call. It’s the same thing as before. Just use StartCoroutine() outside coroutine flow.

For Example, I use StartShowingCommercialBreak() via the Button.
You can also use it without the button.

Update:

Actually, this problem does not apply to coroutines only. You just need to avoid recursion with no end. Recursion with simple functions must have an exit condition.

In Coroutines for this task, you can use yield break; instruction to complete Coroutine. However, I would avoid playing recursion with Coroutines.

Filling a bottle should NEVER be a coroutine. Instead, use this far more reliable pattern:

Smoothing movement between any two particular values:

You have currentQuantity and desiredQuantity.

  • only set desiredQuantity
  • the code always moves currentQuantity towards desiredQuantity
  • read currentQuantity for the smoothed value

Works for floats, Vectors, Colors, Quaternions, anything continuous or lerp-able.

The code: SmoothMovement.cs · GitHub

I’ll take that a step further: if you EVER contemplate stopping a coroutine, then you should NOT be using coroutines. Full stop, end of sentence. Don’t do it. You’ll regret it.

Coroutines are NOT always an appropriate solution: know when to use them!

“Why not simply stop having so many coroutines ffs.” - orionsyndrome on Unity3D forums

This is not a very common practice, but what do you think about using async await instead of coroutines?

Also, In Unity 6 we now have a class specifically tailored for Unity: Unity - Scripting API: Awaitable

Equivalent to coroutines in my book. Still not appropriate for many things, such as refilling bottles, opening doors, slewing turrets, turning players slowly, etc.

The reason: coroutines / async fail MISERABLY on the edge cases: restarting, interrupting, etc.

The edge cases still exist but they are far easier to reason about.

So here’s my Jetpack Kurt refueler:

  • you call it with either “Load X fuel”

It sets that fuel aside in a float and every update it adds the fueling rate (RateOfRefueling * Time.deltaTime) to the fuel tank, decrementing the float by that same amount.

When that float is zero, it is done.

  • or you call it with “Load until full”

It keeps filling at the time-scaled RateOfRefueling until one of those refueling attempts “bumps” the top of the tank capacity, then it stops.

Obviously you can see the baked in assumption behind “fill-er-up:” you MUST take on fuel faster than you can possibly burn it. That’s an easy thing to enforce, generally. Either disable engine while fueling, or make refueling fast.

Edge cases:

You continue burning fuel while refueling - no problem! Works fine, as expected.

If it’s already fueling and you call it again, it reasons about what you want:

if it was told “Fill to full” and then you call it with “fill 10 units” well it just ignores the second.

If it was told “Fill 10 units” and then gets “Fill 20 units” it just adds it on.

If the player gets destroyed, the refueler is ON the player, so it just quietly dies.

FAR far easier to debug than multiple-started coroutines.

1 Like

Coroutines are great when you want mini statemachines. Or want to execute async code in a syncrounous manner. Some good examples from our game.

Waiting for user input in UI

        private IEnumerator DoShowSwitchTeam()
        {
            var result = ShowConfirm(AvatarController.Instance.IsDead() ? "Switch team?" : "You are not dead, you will lose a life. Continue?");
            yield return result;

            if (result.Result == ConfirmResult.OK)
            {
                BaseGameMode.Instance.RequestSwitchTeam(Networking.PrimarySocket.Me.NetworkId, false);
            }
        }

Or when you want a state machine but want to be easy maintainable.

    [CreateAssetMenu(menuName = "Tutorial/AttachAttachmentStep")]
    public class AttachAttachmentStep : InteractStep
    {
        public RailSystemAttachment Prefab;
        public int RailIndex;

        private RailSystemAttachment attachment;
        private RailSystem.RailSystem railSystem;


        public override IEnumerator Execute()
        {
            var firearm = Get<Firearm>();

            topItem = GetAll().Select(r => r.GetComponent<SimpleNetworkedMonoBehavior>()).First(r => r.Prefab.name == Prefab.name).GetComponent<NVRInteractable>();
            railSystem = firearm.GetComponent<RailSystemContainer>().RailSystems[RailIndex];
            attachment = topItem.GetComponent<RailSystemAttachment>();

            yield return Execute(WaitForAttachmentGrab, WaitForAttachmentHoveringOverRail, WaitForPlacementOnRail);
        }

        private IEnumerator WaitForAttachmentGrab()
        {
            if(attachment.AttachedSystem == null)
            { 
                ItemType = "Attachment";
                title = "Attach Attachment";

                yield return base.Execute();
            }
        }

        private IEnumerator WaitForAttachmentHoveringOverRail()
        {
            ShowPopup(railSystem.transform, "Hover over the rail with the attachment.");

            while (attachment.AttachedSystem != railSystem)
            {
                if (attachment.IsCompletelyAttached)
                {
                    yield return Execute<DetachAttachmentStep>(step => { step.Attachment = attachment; step.CorrectAttachmentPlacement = true; });
                    yield return SequenceState.RestartCurrent;
                }
                else
                    yield return null;
            }
        }

        private IEnumerator WaitForPlacementOnRail()
        {
            ShowPopup(railSystem.transform, "Slide the attachment over the rail until you find a good position and let go.");

            while (!attachment.IsCompletelyAttached)
            {
                if (attachment.AttachedSystem != railSystem)
                    yield return SequenceState.Previous;
                else
                    yield return null;
            }
        }
    }