Search code examples
c#unity-game-enginecoroutinecancellation

Exception handling of coroutines in Unity


I created a script that changes the transparency of the GameObject it's attached to and I do the transparency change in a fading coroutine which needs to be cancellable (and canceled everytime we call ChangeTransparency() with a new value). I managed to get it working the way I want it but I want to handle the OperationCanceledException that is flooding my Console. I know that you cannot wrap a yield return statement inside a try-catch block.

What is a proper way to handle Exceptions inside Unity coroutines?

Here is my script:

using System;
using System.Collections;
using System.Threading;
using UnityEngine;

public class Seethrough : MonoBehaviour
{
    private bool isTransparent = false;
    private Renderer componentRenderer;
    private CancellationTokenSource cts;
    private const string materialTransparencyProperty = "_Fade";


    private void Start()
    {
        cts = new CancellationTokenSource();

        componentRenderer = GetComponent<Renderer>();
    }

    public void ChangeTransparency(bool transparent)
    {
        //Avoid to set the same transparency twice
        if (this.isTransparent == transparent) return;

        //Set the new configuration
        this.isTransparent = transparent;

        cts?.Cancel();
        cts = new CancellationTokenSource();

        if (transparent)
        {
            StartCoroutine(FadeTransparency(0.4f, 0.6f, cts.Token));
        }
        else
        {
            StartCoroutine(FadeTransparency(1f, 0.5f, cts.Token));
        }
    }

    private IEnumerator FadeTransparency(float targetValue, float duration, CancellationToken token)
    {
        Material[] materials = componentRenderer.materials;
        foreach (Material material in materials)
        {
            float startValue = material.GetFloat(materialTransparencyProperty);
            float time = 0;

            while (time < duration)
            {
                token.ThrowIfCancellationRequested();  // I would like to handle this exception somehow while still canceling the coroutine

                material.SetFloat(materialTransparencyProperty, Mathf.Lerp(startValue, targetValue, time / duration));
                time += Time.deltaTime;
                yield return null;
            }

            material.SetFloat(materialTransparencyProperty, targetValue);
        }
    }
}

My temporary solution was to check for the token's cancellation flag and break out of the while loop. While it solved this current problem, I am still in need of a way to handle these exceptions that are thrown in asynchronous methods (coroutines) in Unity.


Solution

  • The other answer is good and all but actually not really on point for the use case you are trying to solve.

    You don't have any async code but need the code to be executed in the Unity main thread since it is using Unity API and most of it can only be used on the main thread.

    A Coroutine is NOT ASYNC and has nothing todo with multithreading or Tasks!

    A Coroutine is basically just an IEnumerator as soon as it is registered as a Coroutine by StartCorotuine then Unity calls MoveNext on it (which will execute everything until the next yield return statement) right after Update so once a frame (there are some special yield statements like WaitForFixedUpdate etc which are handled in different message stacks but still) on the Unity main thread.

    So e.g.

    // Tells Unity to "pause" the routine here, render this frame
    // and continue from here in the next frame
    yield return null;
    

    or

    // Will once a frame check if the provided time in seconds has already exceeded
    // if so continues in the next frame, if not checks again in the next frame
    yield return new WaitForSeconds(3f);
    

    If you really need to somehow react to the cancelation within the routine itself there speaks nothing against your approach except, instead of immediately throw an exception rather only check the CancellationToken.IsCancellationRequested and react to it like

    while (time < duration)
    {
        if(token.IsCancellationRequested)
        {
            // Handle the cancelation
    
            // exits the Coroutine / IEnumerator regardless of how deep it is nested in loops
            // not to confuse with only "break" which would only exit the while loop
            yield break;
        }
                
        ....
    

    To answer also on your title. It is true you can't do yield return inside a try - catch block and as said in general there isn't really a reason to do so anyway.

    If you really need to you can wrap some parts of your code without the yield return in a try - catch block and in the catch you do your error handling and there you can again yield break.

    private IEnumerator SomeRoutine()
    {
        while(true)
        {
            try
            {
                if(Random.Range(0, 10) == 9) throw new Exception ("Plop!");
            }
            catch(Exception e)
            {
                Debug.LogException(e);
                yield break;
            }
    
            yield return null;
        }
    }