Game Dev Cheat Sheet

Unity Coroutine vs Async/Await

Side-by-side comparison of Unity coroutines and async/await with code for every common pattern.

Unity introduced Awaitable in 2023.1. Compare coroutine and async implementations of waits, sequences, cancellation and return values.

Wait for Seconds

Coroutine

IEnumerator Wait()
{
    yield return new WaitForSeconds(2f);
    Debug.Log("Done waiting");
}

Async/Awaitable

async Awaitable WaitAsync()
{
    await Awaitable.WaitForSecondsAsync(2f);
    Debug.Log("Done waiting");
}

Wait for Condition

Coroutine

IEnumerator WaitFor()
{
    yield return new WaitUntil(() => isDone);
    Debug.Log("Condition met");
}

Async/Awaitable

async Awaitable WaitForAsync()
{
    while (!isDone)
        await Awaitable.NextFrameAsync();
    Debug.Log("Condition met");
}
// Unity's built-in Awaitable does not include
// a WaitUntilAsync. Use a NextFrameAsync loop
// as shown, or use UniTask's
// UniTask.WaitUntil() if you have it installed.

Sequential Operations

Coroutine

IEnumerator RunSequence()
{
    yield return StartCoroutine(StepA());
    yield return StartCoroutine(StepB());
    yield return StartCoroutine(StepC());
    Debug.Log("All steps complete");
}

Async/Awaitable

async Awaitable RunSequenceAsync()
{
    await StepAAsync();
    await StepBAsync();
    await StepCAsync();
    Debug.Log("All steps complete");
}

Parallel Operations

Coroutine

IEnumerator RunParallel()
{
    bool aFinished = false;
    bool bFinished = false;

    StartCoroutine(DoA(() => aFinished = true));
    StartCoroutine(DoB(() => bFinished = true));

    yield return new WaitUntil(
        () => aFinished && bFinished
    );
    Debug.Log("Both complete");
}

Async/Awaitable

async Awaitable RunParallelAsync()
{
    // Awaitable does not support WhenAll natively.
    // Use Task.WhenAll by returning Task from each
    // method, or use UniTask.WhenAll if installed.
    Task a = DoAAsync();
    Task b = DoBAsync();
    await Task.WhenAll(a, b);
    Debug.Log("Both complete");
}
// DoAAsync and DoBAsync must return Task here.
// Unity Awaitable has no built-in AsTask conversion.
// For Awaitable methods, start both and await each once.

Cancellation

Coroutine

Coroutine routine;

void Start()
{
    routine = StartCoroutine(MyRoutine());
}

void Cancel()
{
    // No cleanup hook; just stops
    StopCoroutine(routine);
}

Async/Awaitable

CancellationTokenSource cts;

async void Start()
{
    cts = CancellationTokenSource.CreateLinkedTokenSource(
        destroyCancellationToken);
    try
    {
        await MyAsync(cts.Token);
    }
    catch (OperationCanceledException)
    {
        Debug.Log("Cancelled cleanly");
    }
    catch (Exception exception)
    {
        Debug.LogException(exception);
    }
    finally
    {
        cts.Dispose();
        cts = null;
    }
}
// Use async void for this Unity entry point, handling
// exceptions here. Return Awaitable or Task from helpers.
// Requires System and System.Threading.

void Cancel()
{
    cts?.Cancel();
}

Return a Value

Coroutine

// Coroutines cannot return values directly.
// Use a callback or set a field instead.
IEnumerator LoadHP(System.Action<int> callback)
{
    yield return new WaitForSeconds(1f);
    callback(100);
}

// Usage
StartCoroutine(LoadHP(hp =>
{
    Debug.Log("HP: " + hp);
}));

Async/Awaitable

// Awaitable<T> returns a value directly.
async Awaitable<int> LoadHPAsync()
{
    await Awaitable.WaitForSecondsAsync(1f);
    return 100;
}

// Usage
int hp = await LoadHPAsync();
Debug.Log("HP: " + hp);

Summary Comparison

FeatureCoroutineAsync/Awaitable
Return valuesNo (use callbacks)Yes
Exception handlingCannot yield inside try/catchFull try/catch/finally
CancellationStopCoroutine (no cleanup)CancellationToken (clean)
Parallel executionManual trackingTask.WhenAll for Task-returning methods
Requires MonoBehaviourYesNo
Unit testableDifficultStandard async testing
Available sinceEarlier Unity versionsUnity 2023.1+ (Awaitable) / UniTask for earlier versions

Quick decision guide

  • Use coroutines when: simple time-based sequences, working with existing coroutine code, targeting older Unity versions.
  • Use async/await when: starting new code on Unity 2023.1+, need exception handling, want to use Task-based libraries, need cancellation tokens.
  • Mix freely. Both are supported. Use an explicit wrapper when bridging between coroutine and async code.

Frequently asked questions

Should I use coroutines or async/await in Unity?
For new code in Unity 2023.1 and later, async/await with Awaitable is generally preferred: better debugging, exception handling, and integration with the C# ecosystem. Coroutines remain a fine choice for simple time-based sequences and are still well supported. Mix freely; avoid converting working code without reason.
Are async methods more expensive than coroutines?
Modern async/await in Unity has comparable or lower allocation cost than coroutines, especially with Awaitable. Older Task-based async could allocate more, but Awaitable was designed to fix this. Profile before assuming either is the bottleneck.
Can I await a coroutine?
Not directly. You can wrap a coroutine in a TaskCompletionSource or convert to Awaitable, but the simpler approach is to convert the coroutine to async. If you need both, expose two entry points that share a private async implementation.
What happens to a coroutine when the GameObject is disabled?
Deactivating the GameObject stops its coroutines. Async operations need explicit cancellation. Pass destroyCancellationToken to operations that should stop on destruction; use a separate cancellation source if they should also stop on disable. A disabled object still exists and does not by itself cause MissingReferenceException.

Last updated: