C
C#β€’4w ago
Ruttie

Try-catch in an iterator

Is it possible to do a try-catch, or alternatively a try-finally over a yield return statement? When I try something like this:
IEnumerator Method1(IDisposable disp) {
try {
yield return Method2();
}
finally {
disp.Dispose();
}
}
IEnumerator Method1(IDisposable disp) {
try {
yield return Method2();
}
finally {
disp.Dispose();
}
}
then the Dispose method is never called.
49 Replies
TheBoxyBear
TheBoxyBearβ€’4w ago
There actually seems to be conflicting information on this matter. It seems in the case of an iterator finally is called on the disposing of the enumerator :hmm: Yet the documentation doesn't explain the behavior of yield in try-finally, instead claiming it's not allowed at all while it's actually only disallowed when a catch is present https://learn.microsoft.com/dotnet/csharp/language-reference/statements/yield
yield statement - provide the next element in an iterator - C# refe...
Use the yield statement in iterators to provide the next value or signal the end of an iteration
TheBoxyBear
TheBoxyBearβ€’4w ago
@Ruttie Try disposing the enumerator mid enumeration and see if finally runs then?
Ruttie
RuttieOPβ€’4w ago
I think I managed to solve the problem. By manually iterating over the enumerator returned by Method2 and try-catching only the movenext, I can then record the exception though this only works for one 'level' of enumerator
TheBoxyBear
TheBoxyBearβ€’4w ago
Are you expecting MoveNext to throw an exception? It typically just returns false in the worst case Actually nevermind mutable collections to throw exceptions when modified
Ruttie
RuttieOPβ€’4w ago
well, it should be noted that Method2 does not return something like a list, but rather is an enumerator itself if that makes sense this is all in the context of unity coroutines
TheBoxyBear
TheBoxyBearβ€’4w ago
Oh didn't notice the yield was returning from a call So essentially doing embeded coroutines?
Ruttie
RuttieOPβ€’4w ago
yeah the problem is that I don't want the main method to exit if Method2 throws an exception, but I can't simply do a try-catch around it which has currently resulted in me using a task completion source to observe the coroutine, and running Method2 in a separate coroutine πŸ˜…
TheBoxyBear
TheBoxyBearβ€’4w ago
So if both methods are intended as coroutines, it seems strange to call it in a yield. It's essentially starting coroutine 2 every time except the return will be an enumerator rather than an expected coroutine type like WaitForSeconds
Ruttie
RuttieOPβ€’4w ago
in which the try-finally is useful since I had hoped that would notify the completion source even if Method2 throws
TheBoxyBear
TheBoxyBearβ€’4w ago
Then you can always use try-catch to store the result and yield afterwards if no exception was thrown, otherwise skip the item
MODiX
MODiXβ€’4w ago
erπŸŽƒ
REPL Result: Success
IEnumerable<int> M()
{
try
{
int i = 0;
while (true)
yield return i++;
}
finally
{
Console.WriteLine("Enumerator ended.");
}
}

M().Take(10).ToList()
IEnumerable<int> M()
{
try
{
int i = 0;
while (true)
yield return i++;
}
finally
{
Console.WriteLine("Enumerator ended.");
}
}

M().Take(10).ToList()
Result: List<int>
[
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
]
[
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
]
Console Output
Enumerator ended.
Enumerator ended.
Compile: 499.978ms | Execution: 90.990ms | React with ❌ to remove this embed.
ero
eroβ€’4w ago
dispose should be called fine
Ruttie
RuttieOPβ€’4w ago
that doesn't actually work, since that will only catch any exceptions that happen before the first yield return
TheBoxyBear
TheBoxyBearβ€’4w ago
Design wise it also seems strange that an enumerator throwing an exception on MoveNext would still be valid to continue enumeration
Ruttie
RuttieOPβ€’4w ago
I think unity may also be the problem, I thought it may be killing my coroutine on the exception I'm not continuing the Method2 enumerator after an exception
ero
eroβ€’4w ago
unity shouldn't be the problem i mean maybe but not likely
TheBoxyBear
TheBoxyBearβ€’4w ago
I highly doubt unity doesn't auto dispose the enumerator after a coroutine ends
Ruttie
RuttieOPβ€’4w ago
I mean, this is what I tried, and it doesn't work
ero
eroβ€’4w ago
we would need to know how you tried that
Ruttie
RuttieOPβ€’4w ago
wdym?
ero
eroβ€’4w ago
how are you calling the method
Ruttie
RuttieOPβ€’4w ago
Method1 is going into a StartCoroutine call note that Method1 also has a yield return null; at the start, so that control is immediately given to unity
TheBoxyBear
TheBoxyBearβ€’4w ago
Does Unity actually support yielding enumerators as individual items? :hmm: (for couroutines)
Ruttie
RuttieOPβ€’4w ago
yup
TheBoxyBear
TheBoxyBearβ€’4w ago
Strange I can't find docs on that behavior, I just assumed you'd need to manually iterate Method2 and yield each item If that unrolling is handled by unity, then that may be another source of the problem. The Method2 call in itself doesn't actually run to the end and instead exists immediately after creating the iterator. You wouldn't be getting MoveNext exceptions until that gets unrolled
ero
eroβ€’4w ago
not that i can tell?
TheBoxyBear
TheBoxyBearβ€’4w ago
Either way a safer way of doing a try on Method2's MoveNext would be manually enumerating it from Method1 and yielding each item.
Ruttie
RuttieOPβ€’4w ago
yeah, that is what I'm doing
TheBoxyBear
TheBoxyBearβ€’4w ago
With yield return Method2(); or something else?
Ruttie
RuttieOPβ€’4w ago
specifically:
yield return null;
IEnumerator coro;
try {
coro = Method2();
}
catch (Exception ex) {
obj._tcs.SetException(ex);
yield break;
}

while (true)
{
object current = null;
try
{
if (!coro.MoveNext())
break;

current = coro.Current;
}
catch (Exception ex)
{
obj._tcs.SetException(ex);
yield break;
}

yield return current;
}

obj.Complete();
yield return null;
IEnumerator coro;
try {
coro = Method2();
}
catch (Exception ex) {
obj._tcs.SetException(ex);
yield break;
}

while (true)
{
object current = null;
try
{
if (!coro.MoveNext())
break;

current = coro.Current;
}
catch (Exception ex)
{
obj._tcs.SetException(ex);
yield break;
}

yield return current;
}

obj.Complete();
TheBoxyBear
TheBoxyBearβ€’4w ago
So what's wrong with this? You no longer try to dispose anything or have finally
Ruttie
RuttieOPβ€’4w ago
wdym? I never said this was wrong, I explicitly said:
I think I managed to solve the problem.
TheBoxyBear
TheBoxyBearβ€’4w ago
Right Then what do you mean by only working with one level? You could always rethrow the exception to have it handled by a higher level
Ruttie
RuttieOPβ€’4w ago
well, if Method2 yield returns another enumerator, and that enumerator throws, my code won't work anymore
TheBoxyBear
TheBoxyBearβ€’4w ago
The new snippet has Method1 return items from Method2
Ruttie
RuttieOPβ€’4w ago
yes one of the items returned from Method2 can be an IEnumerator which would then basically be equivalent to putting a yield return Method3() in Method1 thus causing the same issue again
TheBoxyBear
TheBoxyBearβ€’4w ago
So do you want the items fully flattened by the end? You can always check the type of the current object against IEnumerator and enumerate that
ero
eroβ€’4w ago
well they're coroutines, there aren't really any "items" but you can think of it that way i guess
TheBoxyBear
TheBoxyBearβ€’4w ago
The wait requests* ig
ero
eroβ€’4w ago
they just want to catch any exceptions thrown by any coroutines i guess
TheBoxyBear
TheBoxyBearβ€’4w ago
You could also let the methods yield enumerators and only try at the highest level, then flatten recursively for every item that's an enumerator, but then you couldn't start coroutines on individual methods and woulld need to instead wrap it in a flattenner helper method.
ero
eroβ€’4w ago
internal static class CoroutineExtensions
{
public static IEnumerator HandleExceptions(this IEnumerator routine)
{
while (true)
{
try
{
if (!routine.MoveNext())
{
yield break;
}
}
catch (Exception e)
{
Debug.LogException(e);
}

object current = routine.Current;
if (current is IEnumerator subRoutine)
{
yield return HandleExceptions(subRoutine);
}
else
{
yield return current;
}
}
}
}
internal static class CoroutineExtensions
{
public static IEnumerator HandleExceptions(this IEnumerator routine)
{
while (true)
{
try
{
if (!routine.MoveNext())
{
yield break;
}
}
catch (Exception e)
{
Debug.LogException(e);
}

object current = routine.Current;
if (current is IEnumerator subRoutine)
{
yield return HandleExceptions(subRoutine);
}
else
{
yield return current;
}
}
}
}
StartCoroutine(MyCoroutine().HandleExceptions());
TheBoxyBear
TheBoxyBearβ€’4w ago
^ Though woudln't the internal call to HandleException need to be enumerated as well instead of yielded?
ero
eroβ€’4w ago
?
TheBoxyBear
TheBoxyBearβ€’4w ago
yield return HandleExceptions(subRoutine); On the first level, it would send an enumerator to the coroutine rather than Wait objects
ero
eroβ€’4w ago
yeah it works fine actually i think in past versions you had to wrap it in StartCoroutine as well but it works
TheBoxyBear
TheBoxyBearβ€’4w ago
That's what I was wondering, I couldn't find docs about unity flattening subroutines you send it
ero
eroβ€’4w ago
don't have any either lol i just tested it
TheBoxyBear
TheBoxyBearβ€’4w ago
Although if some subroutines are known to not throw exceptions, it might be worth letting them yield from HandleExceptions rather than forcing it at the root Or have a parameter to choose the behavior
Want results from more Discord servers?
Add your server