UniTask — Zero-Allocation Async/Await for Unity
UniTask (Cysharp) provides async/await that integrates natively with Unity's PlayerLoop, produces zero GC allocations, and supports proper cancellation. Prefer UniTask over coroutines and System.Threading.Tasks.Task in all Unity projects.
UniTask vs Coroutines vs System.Threading.Tasks.Task
| Feature | Coroutine | Task | UniTask | |---------|-----------|------|---------| | GC allocation | Enumerator + box | Task object + state machine | Zero (struct-based) | | Cancellation | Manual flag | CancellationToken | CancellationToken | | Return values | No | Yes | Yes | | Exception handling | Swallowed silently | try/catch | try/catch | | Runs on thread pool | No | Yes (dangerous in Unity) | No (PlayerLoop) | | Awaitable | No | Yes | Yes |
Basic Usage
Method Signatures
using Cysharp.Threading.Tasks;
// Awaitable, returns nothing
public async UniTask LoadLevelAsync(CancellationToken ct)
{
await UniTask.Delay(1000, cancellationToken: ct);
}
// Awaitable, returns a value
public async UniTask<int> CalculateScoreAsync(CancellationToken ct)
{
await UniTask.Yield(ct);
return 100;
}
// Fire-and-forget (use sparingly, only at call boundaries)
public async UniTaskVoid OnButtonClickedAsync()
{
await DoSomethingAsync(this.GetCancellationTokenOnDestroy());
}
CRITICAL: Never Use async void
// BAD — exceptions silently swallowed, no cancellation, GC allocation
public async void DoSomething() { ... }
// GOOD — proper error propagation, zero alloc
public async UniTask DoSomethingAsync(CancellationToken ct) { ... }
// GOOD — fire-and-forget with error logging
public async UniTaskVoid DoSomethingFireAndForget() { ... }
Waiting and Delays
// Time-based delays
await UniTask.Delay(1000, cancellationToken: ct); // Milliseconds
await UniTask.Delay(TimeSpan.FromSeconds(1.5f), cancellationToken: ct);
// Frame-based waits
await UniTask.Yield(); // Next frame
await UniTask.Yield(PlayerLoopTiming.FixedUpdate); // Next FixedUpdate
await UniTask.NextFrame(ct); // Explicit next frame
await UniTask.DelayFrame(5, cancellationToken: ct); // Wait N frames
// Condition waits
await UniTask.WaitUntil(() => _isReady, cancellationToken: ct);
await UniTask.WaitWhile(() => _isLoading, cancellationToken: ct);
await UniTask.WaitUntilValueChanged(transform, t => t.position, cancellationToken: ct);
// Unity async operation wrappers
await SceneManager.LoadSceneAsync("GameScene").ToUniTask(cancellationToken: ct);
await Resources.LoadAsync<Texture2D>("myTexture").ToUniTask(cancellationToken: ct);
await UnityWebRequest.Get(url).SendWebRequest().ToUniTask(cancellationToken: ct);
Cancellation Tokens
CRITICAL: Always Pass Cancellation Tokens
Async operations that outlive their owning object cause MissingReferenceException and undefined behavior. Every async method must accept and respect a CancellationToken.
Pattern 1: GetCancellationTokenOnDestroy (Simple)
public class SimpleAsync : MonoBehaviour
{
private async UniTaskVoid Start()
{
// Token auto-cancels when this MonoBehaviour is destroyed
CancellationToken ct = this.GetCancellationTokenOnDestroy();
await UniTask.Delay(2000, cancellationToken: ct);
Debug.Log("This won't run if object was destroyed");
}
}
Pattern 2: Manual CancellationTokenSource (Enable/Disable)
public class ManagedAsync : MonoBehaviour
{
private CancellationTokenSource _cts;
private void OnEnable()
{
_cts = new CancellationTokenSource();
RunLoopAsync(_cts.Token).Forget();
}
private void OnDisable()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
private async UniTask RunLoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await UniTask.Delay(1000, cancellationToken: ct);
DoPeriodicWork();
}
}
}
Pattern 3: Linked Tokens (Combine Destroy + Manual Cancel)
public class LinkedTokenExample : MonoBehaviour
{
private CancellationTokenSource _actionCts;
public async UniTask PerformActionAsync()
{
// Cancel previous action if still running
_actionCts?.Cancel();
_actionCts?.Dispose();
_actionCts = new CancellationTokenSource();
// Link with destroy token so it cancels on either condition
CancellationToken destroyCt = this.GetCancellationTokenOnDestroy();
CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(
_actionCts.Token, destroyCt);
try
{
await DoWorkAsync(linked.Token);
}
catch (OperationCanceledException)
{
// Expected on cancellation — do nothing
}
finally
{
linked.Dispose();
}
}
}
Handling OperationCanceledException
public async UniTask LoadDataAsync(CancellationToken ct)
{
try
{
await SomeAsyncOperation(ct);
}
catch (OperationCanceledException)
{
// Normal cancellation — cleanup silently
return;
}
catch (Exception ex)
{
// Actual error — log and handle
Debug.LogException(ex);
}
}
PlayerLoop Integration
UniTask hooks into Unity's PlayerLoop for precise timing control.
// Available timing points
await UniTask.Yield(PlayerLoopTiming.Initialization);
await UniTask.Yield(PlayerLoopTiming.EarlyUpdate);
await UniTask.Yield(PlayerLoopTiming.FixedUpdate);
await UniTask.Yield(PlayerLoopTiming.PreUpdate);
await UniTask.Yield(PlayerLoopTiming.Update);
await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);
await UniTask.Yield(PlayerLoopTiming.PostLateUpdate);
await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate);
// Wait for specific timing in FixedUpdate
await UniTask.WaitForFixedUpdate(ct);
// Wait for end of frame (replacement for WaitForEndOfFrame coroutine)
await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate, ct);
WhenAll / WhenAny — Parallel Execution
// Wait for all tasks to complete (parallel)
(int score, string name) = await UniTask.WhenAll(
LoadScoreAsync(ct),
LoadNameAsync(ct)
);
// Wait for first task to complete
int winnerIndex = await UniTask.WhenAny(
WaitForInputAsync(ct),
WaitForTimeoutAsync(5f, ct)
);
// Typed WhenAny with result
(bool hasResult, int result) = await UniTask.WhenAny(
FetchFromCacheAsync(ct),
FetchFromNetworkAsync(ct)
);
// Load multiple assets in parallel
var textures = await UniTask.WhenAll(
paths.Select(p => LoadTextureAsync(p, ct))
);
Forget and Fire-and-Forget
// Fire and forget — logs exceptions to Debug.LogException
DoSomethingAsync(ct).Forget();
// Suppress specific cancellation exceptions
DoSomethingAsync(ct).SuppressCancellationThrow().Forget();
UniTaskCompletionSource — Manual Completion
For wrapping callback-based APIs or creating custom awaitable operations.
public class DialogSystem : MonoBehaviour
{
private UniTaskCompletionSource<DialogResult> _dialogTcs;
public async UniTask<DialogResult> ShowDialogAsync(string message, CancellationToken ct)
{
_dialogTcs = new UniTaskCompletionSource<DialogResult>();
// Register cancellation
ct.Register(() => _dialogTcs.TrySetCanceled());
ShowDialogUI(message);
return await _dialogTcs.Task;
}
// Called by UI buttons
public void OnConfirmClicked() => _dialogTcs.TrySetResult(DialogResult.Confirm);
public void OnCancelClicked() => _dialogTcs.TrySetResult(DialogResult.Cancel);
}
Async LINQ
UniTask provides async LINQ operators for event streams.
using Cysharp.Threa