Why can't you yield an IEnumerator directly in a Coroutine?

It’s kind of a minor thing - but it’s a mistake I make much more frequently than I’d like. When nesting coroutines, why is it necessary to actually call start coroutine on the subroutines?

protected IEnumerator Start(){
  // why doesn't this work? Worse, it just silently fails to do anything.
  yield return SubRoutine();
}

protected IEnumerator SubRoutine(){ ... }

Why doesn’t the coroutine process just wrap a yielded IEnumerator in a coroutine - since returning an IEnumerator directly doesn’t do anything and clearly isn’t intentional? Do other people make this mistake as often as I do?

To keep myself from trying to start a coroutine without actually using StartCoroutine(), I got into the habit of prefixing coroutine methods with “Run” - that serves as a handy reminder of how the method should be called.

IEnumerators weren’t invented by Unity. They use IEnumerators to make their Coroutine implementation work, so the syntax we’re required to use makes sense to me.

Because SubRoutine() isn’t started yet and doesn’t exist, so there’s nothing to yield on. I never really thought about it, I suppose its somewhat inconvenient? IMO it’s not a big deal.

Can you pass an IEnumerator variable to yield on? That would be the only thing I could think of as why. To differentiate between yielding on some IEnumerator as a variable and starting a new routine. Might be some fringe cases they had to account for here…

I actually use naming conventions here too I always call the IEnumerator methods GetXRoutine() to improve the chances of remember to call start.

It’s not about syntax, it’s about calling Component.StartCoroutine( x )…

Just to be clear for everyone:

If you put this in a mono behaviour, it will work:

protected IEnumerator Start(){
  yield return StartCoroutine(SubRoutine());
}

protected IEnumerator SubRoutine(){
  yield return new WaitForSeconds(1f);
  Debug.Log( "it's one second after this monobehaviour was started" );
}

This on the other hand will do nothing.

protected IEnumerator Start(){
  yield return SubRoutine();
}

protected IEnumerator SubRoutine(){
  yield return new WaitForSeconds(1f);
  Debug.Log( "it's one second after this monobehaviour was started" );
}

To understand this, you have to know what an IEnumerator actually is. As @BenZed said, Unity didn’t invent them, and in fact, their usage in Unity is really, really bizarre and kind of a hack. Let’s start with a more accurate definition of a coroutine. You probably think of it something like: “A function that can take multiple frames to execute”. But think about that, and then remember that coroutines exist outside Unity. There’s no such thing as a “frame”, inherently, outside of Unity. So what is a coroutine, really?

A normal function does some stuff, and then returns a value, and then it’s done. A coroutine does some stuff, returns a value, but is not necessarily done. Instead, it “yields”. When it returns, it sends control back to whatever called it; it’s saying “Hey, I’m done with the thing I did; I have more stuff to do, though, so I’ll just be waiting here for you to call me.”

So here’s the problem. When you just call “yield return SubRoutine();”, you never do anything else with that. The coroutine yield returns, and then it’s just gone. It’s like a line of code that looks like:

Vector3.Distance(Vector3.zero, transform.position);

Sure, this is a valid function call, but it spits out a number and you’re not waiting there with a variable in which to catch it, so it just vanishes. And, like your example, it fails silently.

That’s where StartCoroutine comes in. StartCoroutine grabs that yielded coroutine and stashes it in Unity’s internals. Unity, once a frame, crawls through this stash and resumes each function that was waiting. Unity has to go out of its way to make this happen; it doesn’t happen automatically, just because a coroutine exists.

3 Likes

This is not accurate - it’s not “gone” its just dropped by Unity’s internal coroutine processing because IEnumerator isn’t a subclass of unity’s YieldInstruction.

The iterator that runs processes each of the yield instructors off of the IEnumerator on a per frame basis. If it encounters a non yield instruction it will just drop the result and continue next frame. The point is that if it encounters a IEnumerator as a yielded value here - it will always have been a mistake (since a yielded IEnumerator does absolutely nothing otherwise). The iterator could easily correct this mistake by rewrapping the IEnumerator in a coroutine that’s processed off whatever component is currently responsible for the coroutines processing.

That instead of just rewrapping the yielded IEnumerator in a coroutine - it simply drops the result, which is kind of pointless.

1 Like

Just because iterator functions aren’t a unity design, doesn’t mean unity can’t auto start the next coroutine…

here is my RadicalCoroutine that adds pausing, custom yield instructions, and other stuff to coroutines. Including the ability to just return an IEnumerator and have it automatically treated as part of the coroutine:

I treat the return of an IEnumerator as saying “I’m expanding this routine to include nested subroutine, if I stop, then my subroutines stop”. Where as returning a Coroutine or RadicalCoroutine is treated as “I’m wanting to wait until this other independent coroutine is finished, if I stop, that coroutine can continue on, as its independent”.

3 Likes

I’m not sure I follow - so you treat an IEnumerator as a sub process run in parallel instead of in sequence?

If the parent process will complete in 10 frames, but the child will require 20 frames - will the child never complete under any circumstance? Or will the sub routine only be stopped if someone explicitly calls “stop” on it?

no, in sequence.

Just like if you were to call a function, it blocks the calling function until it completes.

1 Like

@lordofduct 's code is a solid example. If you look at line 582 you’ll see a nice example of the logic.

The point though is that yielding an IEnumerator is, I believe, unambiguously an example of a programmer forgetting to call StartCoroutine(). Is there any reason that unity’s iterator shouldn’t correct this by wrapping an IEnumerator in a coroutine?

I guarantee you that I’m not the only programmer who has passed over a “I forgot to start that routine” bug, and I’m certainly not the last. If there is never a time when a yielded IEnumerator is not intended as a sub coroutine, why not just fix the processing logic ?

If we are going to start picking holes in Unity’s implementation of coroutines this probably wouldn’t be the first place I’d start. A better implementation might be using IEnumerator. Coroutine would have an implicit cast to YieldInstruction, allowing you to yield coroutines. Yielding anything else would cause a compiler error.

Sure Unity could do some more stuff in coroutines to deal with your particular issue. But I would honestly think moving to a more solid C# implementation using generics would be better. It would probably be slightly faster (avoids a bunch of casting and null checks to see if a YieldInstruction was yielded). It would also be more obvious what what happening. And compile time errors are much better to deal with then the silent fail that now happens with coroutines.

1 Like

For anyone reading this now: Unity seems to have changed this behaviour since this post was written. The example given above:

    void Start()
    {
        StartCoroutine(Routine());
    }

    protected IEnumerator Routine()
    {
        yield return SubRoutine();
    }

    protected IEnumerator SubRoutine()
    {
        Debug.Log("In the SubRoutine");
        yield return new WaitForSeconds(4);
        Debug.Log("Goodby");
    }

Will now log “In the SubRoutine” and, four seconds later, “Goodby”.

I can’t find this in Unity documentation though, and I don’t know in which version this became possible.

2 Likes

Yeah, Unity changed the behaviour several years ago. I don’t remember exactly when it was, maybe Unity 2017 or so?

Way back when even before this thread started I had written my own ‘RadicalCoroutine’ that did this, as well as custom yield instructions, canceling, pausing, etc. Unity has basically added most of those features now making my RadicalCoroutine almost obsolete.

So now yield an IEnumerator directly in a Coroutine is a normal thing to do?

And what is the difference now between:

yield return SubRoutine();
yield return StartCoroutine(SubRoutine());

The difference is that in the first case the code in the subroutine will not start running until “later” (slightly unclear but probably the next time the normal “once per frame” checks happen for coroutines.)
In the second example the subroutine will start executing immediately up until the first yield statement.

Edit: ignore me, read Bunny83’s answer below.

Where did you get that from? As far as I can tell a nested sub coroutine is immediately run up to the first yield. Try this example:

    private void Start()
    {
        Debug.Log("StartCoroutine");
        StartCoroutine(SomeCoroutine());
        Debug.Log("StartCoroutine done");
    }
    IEnumerator SomeCoroutine()
    {
        Debug.Log("SomeCoroutine frame:" + Time.frameCount);
        yield return SomeNestedCoroutine(10);
    }

    IEnumerator SomeNestedCoroutine(int depth)
    {
        Debug.Log("Nested frame:" + Time.frameCount+" depth: " + depth);
        if (depth >0)
            yield return SomeNestedCoroutine(depth - 1);
    }

As you can see I deliberately called the same coroutine recursively 10 times. So they nest 10 levels deep. You will notice that all run in the same frame without any delay. They are actually executed from within StartCoroutine call. The log actually looks like this:

StartCoroutine
SomeCoroutine frame:1
Nested frame:1 depth: 10
Nested frame:1 depth: 9
Nested frame:1 depth: 8
Nested frame:1 depth: 7
Nested frame:1 depth: 6
Nested frame:1 depth: 5
Nested frame:1 depth: 4
Nested frame:1 depth: 3
Nested frame:1 depth: 2
Nested frame:1 depth: 1
Nested frame:1 depth: 0
StartCoroutine done

As you can see all the nested coroutines start right away. There is however an actual difference between using StartCoroutine and running an IEnumerator as a nested sub coroutine. When you use StartCoroutine, that coroutine would run independently from the outer coroutine. The outer coroutine simply waits for the completion of the nested coroutine before it continues. When you don’t use StartCoroutine, the nested sub corouine would actually run inside the “same” coroutine. So stopping the outer coroutine would completely stop the coroutine as well as any nested coroutine. That’s because Unity probably implemented something like a Stack<IEnumerator> internally inside the Coroutine. So when a coroutine yields an IEnumerator, it essentially pushes that “sub” IEnumerator onto the stack and starts iterating this instead. When an IEnumerator is finished it’s simply popped off the stack and the last one is continued. This runs until the stack is empty. So directly yielding an IEnumerator is most likely cheaper.

1 Like

A vague remembrance from an earlier discussion on this several years ago that apparently was not correct.

Thanks for the demo.

1 Like

This is not the behaviour I’m seeing in 6000.1.6f1, the nested coroutine is not stopped when stopping the outermost one. The nested coroutine is running independently.

Here is a reproduction case:

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
	Coroutine coroutine;

	void Start ()
	{
		timeAtStart = Time.time;
		coroutine = StartCoroutine ( FirstSequence () );
	}

	IEnumerator FirstSequence ()
	{
		// work as expected if using this code:
		// var sequence = SecondSequence();
		// while ( sequence.MoveNext() )
		// 	yield return sequence.Current;

		// don't work as expected when using this
		yield return SecondSequence ();

		Debug.Log ( "Finished" );
	}

	IEnumerator SecondSequence()
	{
		while ( enabled )
		{
			StopTheCoroutineAfterTime ();
			yield return new WaitForSeconds ( 0.5f );
			Debug.Log ( $"{Time.time}" );
		}
	}

	float timeAtStart;

	void StopTheCoroutineAfterTime ()
	{
		if ( Time.time - timeAtStart > 2 )
		{
			if ( coroutine != null )
			{
				Debug.Log ( "The coroutine is being stopped now" );
				StopCoroutine ( coroutine );
				coroutine = null;
			}
		}
	}
}

The SecondSequence code will run forever, and never stop even though the coroutine has been stopped.

I’m not totally sure if it is expected. Not by me at least, and it seems that not by you Bunny. In a way I can understand why this code behaves like it does, I can guess of an implementation that explain that, but I was not expecting that.

Note the workaround I commented, which consists into enumerating the SecondSequence ourselves. In this case, the expected behaviour happens : the whole execution is stopped after a call to StopCoroutine.

Also note that StopAllCoroutines will stop the SecondSequence coroutine (the one created on the fly by Unity) as expected.

I’m not sure if I should report that as a bug to Unity devs, as documentation (as often) is not clear at all about the behaviour to expect (side note to Unity devs : please halt producing code for a while and work on documenting the existing one!!).

EDIT : I just tested it on Unity 2021.3.38f1, the same behaviour happens, so I guess it is “not a bug”. I’m confused. I will use my workaround, that way at least I’m sure I don’t create any other coroutines, and won’t get ‘detached’ coroutines.

I guess we just had a collective mis-expectation thinking that nested coroutines created on the fly by Unity code would be linked to their outer coroutine. But they are not.

Honestly its 2025 and you’re better off using async code for anything that needs to run longer than a frame with any degree of complexity in this day and age. You then at least have full control over when something should stop.

3 Likes