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
| Feature | Coroutine | Async/Awaitable |
|---|---|---|
| Return values | No (use callbacks) | Yes |
| Exception handling | Cannot yield inside try/catch | Full try/catch/finally |
| Cancellation | StopCoroutine (no cleanup) | CancellationToken (clean) |
| Parallel execution | Manual tracking | Task.WhenAll for Task-returning methods |
| Requires MonoBehaviour | Yes | No |
| Unit testable | Difficult | Standard async testing |
| Available since | Earlier Unity versions | Unity 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.
Related Tools
Scripting Order
Interactive Unity MonoBehaviour lifecycle diagram. When does Awake, Start, Update, and every callback run.
DOTween Reference
Searchable DOTween cheat sheet with copy-paste Unity C# code for every common tween method.
Object Pool Generator
Generate a Unity pooling class with optional callbacks and timed returns.
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: