unity-async-patterns
>
Works with
---
name: unity-async-patterns
description: >
license: MIT
---
# Async & Coroutine Patterns -- Correctness Patterns
> **Prerequisite skills:** `unity-scripting` (coroutines, Awaitable API, yield types), `unity-lifecycle` (destruction timing, destroyCancellationToken)
These patterns target async bugs that are especially dangerous because they often work during testing and fail in production: exceptions silently swallowed, objects destroyed mid-await, and thread context violations.
---
## PATTERN: Awaitable Double-Await
WHEN: Storing an `Awaitable` instance and awaiting it more than once
WRONG (Claude default):
```csharp
Awaitable task = Awaitable.WaitForSecondsAsync(2f);
await task; // First await -- works
await task; // Second await -- UNDEFINED BEHAVIOR (may complete instantly or throw)
```
RIGHT:
```csharp
// Awaitable is POOLED -- after the first await completes, the instance is recycled
// Each Awaitable should be awaited exactly once
// If you need to await the same operation from multiple places, use .AsTask():
var task = Awaitable.WaitForSecondsAsync(2f).AsTask();
await task; // Works
await task; // Works -- Task is not pooled
// Or simply create separate Awaitables:
await Awaitable.WaitForSecondsAsync(2f);
await Awaitable.WaitForSecondsAsync(2f); // Fresh instance
```
GOTCHA: Unity pools `Awaitable` instances to avoid allocation. After completion, the instance is returned to the pool and may be reused by a completely different operation. A second `await` on the same instance may see a different operation's state, complete instantly, or throw. This is unlike `Task` which can be safely awaited multiple times. Use `.AsTask()` when you need multi-await semantics, but be aware this allocates.
---
## PATTERN: Missing destroyCancellationToken
WHEN: Writing async methods in MonoBehaviours
WRONG (Claude default):
```csharp
async Awaitable Start()
{
await Awaitable.WaitForSecondsAsync(5f);
// If object was destroyed during the wait:
// - MissingReferenceException on next Unity API call
// - Or worse: silently operates on a "fake-null" object
transform.position = Vector3.zero;
}
```
RIGHT:
```csharp
async Awaitable Start()
{
try
{
await Awaitable.WaitForSecondsAsync(5f, destroyCancellationToken);
transform.position = Vector3.zero;
}
catch (OperationCanceledException)
{
// Object was destroyed -- this is expected, not an error
}
}
// For methods that chain multiple awaits:
async Awaitable DoMultiStepWork()
{
var token = destroyCancellationToken;
await Awaitable.NextFrameAsync(token);
ProcessStep1();
await Awaitable.WaitForSecondsAsync(1f, token);
ProcessStep2(); // Safe: would have thrown before reaching here if destroyed
await LoadAssetAsync(token);
ProcessStep3();
}
```
GOTCHA: `destroyCancellationToken` is a property on `MonoBehaviour` that triggers when `OnDestroy` begins. Every `Awaitable` wait method accepts an optional `CancellationToken`. Without it, the await completes normally even after the object is destroyed, leading to `MissingReferenceException`. Always pass the token AND catch `OperationCanceledException`.
---
## PATTERN: Thread Context After BackgroundThreadAsync
WHEN: Returning to Unity APIs after doing work on a background thread
WRONG (Claude default):
```csharp
async Awaitable ProcessData()
{
await Awaitable.BackgroundThreadAsync();
var result = HeavyComputation(); // OK: runs on background thread
// CRASH: Accessing Unity API from background thread
transform.position = new Vector3(result, 0, 0);
}
```
RIGHT:
```csharp
async Awaitable ProcessData()
{
await Awaitable.BackgroundThreadAsync();
var result = HeavyComputation(); // Runs on background thread
await Awaitable.MainThreadAsync(); // Switch BACK to main thread
transform.position = new Vector3(result, 0, 0); // Now safe
// Can switch back and forth:
await Awaitable.BackgroundThreadAsync();
var moreData = AnotherHeavyTask();
await Awaitable.MainThreadAsync();
ApplyResults(moreData);
}
```
GOTCHA: After `BackgroundThreadAsync()`, ALL subsequent code runs on a thread pool thread until you explicitly switch back with `MainThreadAsync()`. Unity APIs (Transform, GameObject, Physics, etc.) are **not thread-safe** and will throw or corrupt state if called from a background thread. `MainThreadAsync()` resumes on the next frame's player loop update, not immediately.
---
## PATTERN: Coroutine Error Swallowing
WHEN: Exceptions occur inside coroutines
WRONG (Claude default):
```csharp
IEnumerator LoadAndProcess()
{
yield return LoadData(); // If this throws, coroutine silently stops
ProcessData(); // Never reached, no error in console (or just a log, no stack)
}
// try/catch doesn't work with yield:
IEnumerator BadErrorHandling()
{
try
{
yield return SomethingDangerous(); // COMPILER ERROR: cannot yield in try block with catch
}
catch (Exception e)
{
Debug.LogError(e);
}
}
```
RIGHT:
```csharp
// Option 1: Use Awaitable instead (proper exception propagation)
async Awaitable LoadAndProcess()
{
try
{
await LoadDataAsync();
ProcessData();
}
catch (Exception e)
{
Debug.LogError($"Load failed: {e}");
}
}
// Option 2: Error handling without yield in the try block
IEnumerator LoadAndProcessCoroutine()
{
bool success = false;
Exception error = null;
// Wrap the yield outside try/catch
yield return LoadDataCoroutine(result =>
{
success = true;
});
// Handle errors after the yield
if (!success)
{
Debug.LogError("Load failed");
yield break;
}
ProcessData();
}
```
GOTCHA: In coroutines, `yield return` cannot appear inside a `try` block that has a `catch` clause (C# language restriction). Exceptions in yielded coroutines are logged to the console but execution silently stops -- no propagation to the caller. The caller's coroutine continues as if the nested one completed. Use `Awaitable` for any operation that can fail and needs error handling.
---
## PATTERN: WaitForEndOfFrame in Batch Mode
WHEN: Using `WaitForEndOfFrame` or `Awaitable.EndOfFrameAsync` in headless/server/test environments
WRONG (Claude default):
```csharp
IEnumerator CaptureScreenshot()
{
yield return new WaitForEndOfFrame(); // HANGS in batch mode (no rendering)
var tex = ScreenCapture.CaptureScreenshotAsTexture();
}
// Same issue with Awaitable:
async Awaitable WaitForRender()
{
await Awaitable.EndOfFrameAsync(); // HANGS in batch mode
}
```
RIGHT:
```csharp
IEnumerator CaptureScreenshot()
{
// Check if we're in batch mode
if (Application.isBatchMode)
{
yield return null; // Just wait one frame instead
Debug.LogWarning("Screenshot not available in batch mode");
yield break;
}
yield return new WaitForEndOfFrame();
var tex = ScreenCapture.CaptureScreenshotAsTexture();
}
// For tests that need frame advancement without rendering:
IEnumerator TestCoroutine()
{
yield return null; // Advances one frame (works in all modes)
// yield return new WaitForFixedUpdate(); // Also works in batch mode
}
```
GOTCHA: `WaitForEndOfFrame` and `EndOfFrameAsync` wait for the rendering phase. In batch mode (`-batchmode` flag), headless servers, and some test runners, there is no rendering -- so these yields never complete and the coroutine/async hangs forever. Use `yield return null` (next Update) or `Awaitable.NextFrameAsync()` for frame advancement that works everywhere.
---
## PATTERN: Nested Coroutine Cancellation
WHEN: Stopping a parent coroutine that launched child coroutines
WRONG (Claude default):
```csharp
Coroutine _mainRoutine;
void Start()
{
_mainRoutine = StartCoroutine(MainLoop());
}
IEnumerator MainLoop()
{
StartCoroutine(SubTaskA()); // Launched independently
StartCoroutine(SubTaskB()); // Launched independently
yield return new WaitForSeconds(10f);
}
void Cancel()
{
StopCoroutine(_mainRoutine);
// SubTaskA and SubTaskB continue running!
}
```
RIGHT:
```csharp
private Coroutine _mainRoutine;
private Coroutine _subA;
private Coroutine _subB;
IEnumerator MainLoop()
{
_subA = StartCoroutine(SubTaskA());
_subB = StartCoroutine(SubTaskB());
yield return new WaitForSeconds(10f);
}
void Cancel()
{
// Must stop each coroutine individually
if (_mainRoutine != null) StopCoroutine(_mainRoutine);
if (_subA != null) StopCoroutine(_subA);
if (_subB != null) StopCoroutine(_subB);
}
// Better: yield return child coroutines (parent owns them)
IEnumerator MainLoopBetter()
{
yield return StartCoroutine(SubTaskA()); // Waits for A, then...
yield return StartCoroutine(SubTaskB()); // Waits for B
// Stopping MainLoopBetter also stops the currently-yielded child
}
```
GOTCHA: `StartCoroutine(SubTask())` launches an **independent** coroutine. `StopCoroutine` only stops the specified coroutine. BUT: `yield return StartCoroutine(SubTask())` makes the parent wait for the child, and stopping the parent also stops the yielded child. The key distinction: `StartCoroutine` without `yield return` = fire-and-forget; with `yield return` = owned by parent. For complex cancellation trees, prefer `Awaitable` with `CancellationToken`.
---
## PATTERN: async void vs async Awaitable
WHEN: Declaring async methods in Unity scripts
WRONG (Claude default):
```csharp
// async void: exceptions crash the application with no way to catch them
async void DoWork()
{
await Awaitable.WaitForSecondsAsync(1f);
throw new Exception("oops"); // UNHANDLED -- crashes the app
}
void Start()
{
DoWork(); // No way to catch the exception from here
}
```
RIGHT:
```csharp
// async Awaitable: proper exception propagation
async Awaitable DoWork()
{
await Awaitable.WaitForSecondsAsync(1f);
throw new Exception("oops"); // Propagates to caller
}
async Awaitable Start()
{
try
{
await DoWork(); // Exception caught here
}
catch (Exception e)
{
Debug.LogError($"Work failed: {e.Message}");
}
}
// async void is ONLY acceptable for Unity event handlers that require void:
// - Button.onClick handlers
// - UnityEvent callbacks
// Even then, wrap the body in try/catch:
async void OnButtonClicked()
{
try
{
await SaveGameAsync();
}
catch (Exception e)
{
Debug.LogError(e);
}
}
```
GOTCHA: `async void` methods propagate exceptions to the `SynchronizationContext`, which in Unity logs them and potentially crashes. `async Awaitable` methods propagate exceptions to the awaiter, allowing proper try/catch. Unity's lifecycle methods (`Start`, `OnEnable`, etc.) can return `Awaitable` -- prefer this over `void` when using async.
---
## PATTERN: Concurrent Awaitable Race Conditions
WHEN: Multiple async operations modify shared state
WRONG (Claude default):
```csharp
// Two async methods writing to the same field
async Awaitable OnClickSearch(string query)
{
var results = await SearchAsync(query); // User types "cat"
_displayedResults = results; // Race: which query wins?
}
// User clicks twice quickly: "cat" then "dog"
// If "dog" returns first, "cat" results overwrite "dog" results
```
RIGHT:
```csharp
private CancellationTokenSource _searchCts;
async Awaitable OnClickSearch(string query)
{
// Cancel the previous search
_searchCts?.Cancel();
_searchCts?.Dispose();
_searchCts = new CancellationTokenSource();
var token = _searchCts.Token;
try
{
var results = await SearchAsync(query, token);
token.ThrowIfCancellationRequested(); // Check before applying
_displayedResults = results; // Only the latest search applies
}
catch (OperationCanceledException)
{
// Previous search cancelled -- expected
}
}
void OnDestroy()
{
_searchCts?.Cancel();
_searchCts?.Dispose();
}
```
GOTCHA: Unlike coroutines (which are single-threaded and frame-sequential), multiple `Awaitable` chains can interleave across frames. The cancel-previous pattern ensures only the most recent operation applies its results. Link the `CancellationTokenSource` token with `destroyCancellationToken` using `CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken)` for automatic cleanup on destroy.
---
## PATTERN: Addressables AsyncOperationHandle Leak
WHEN: Loading assets with Addressables and not releasing them
WRONG (Claude default):
```csharp
async Awaitable LoadEnemy()
{
var handle = Addressables.LoadAssetAsync<GameObject>("enemy_prefab");
var prefab = await handle.Task;
Instantiate(prefab);
// Handle never released -- memory leak!
}
```
RIGHT:
```csharp
private AsyncOperationHandle<GameObject> _enemyHandle;
async Awaitable LoadEnemy()
{
_enemyHandle = Addressables.LoadAssetAsync<GameObject>("enemy_prefab");
var prefab = await _enemyHandle.Task;
Instantiate(prefab);
}
void OnDestroy()
{
// Release when no longer needed
if (_enemyHandle.IsValid())
Addressables.Release(_enemyHandle);
}
// For instantiated objects, use Addressables.InstantiateAsync (auto-tracked):
async Awaitable SpawnEnemy()
{
var handle = Addressables.InstantiateAsync("enemy_prefab", spawnPoint.position, Quaternion.identity);
var instance = await handle.Task;
// When done: Addressables.ReleaseInstance(instance) instead of Destroy
}
```
GOTCHA: Every `Addressables.LoadAssetAsync` call increments a reference count. Without `Addressables.Release`, the asset stays in memory forever. `Addressables.InstantiateAsync` tracks instances automatically -- use `Addressables.ReleaseInstance` instead of `Destroy`. Scene loading with Addressables (`LoadSceneAsync`) auto-releases on scene unload. Releasing a handle with active instances may cause pink/missing material rendering.
---
## PATTERN: UniTask vs Awaitable Selection
WHEN: Choosing an async framework for a Unity project
WRONG (Claude default):
```csharp
// Mixing UniTask and Awaitable in the same method
async UniTask DoWork()
{
await Awaitable.NextFrameAsync(); // Type mismatch: Awaitable in UniTask method
}
```
RIGHT:
```csharp
// Pick ONE async framework per project:
// === Option A: Awaitable (Unity 6+ built-in) ===
// Pros: No dependencies, integrated with Unity lifecycle, pooled (zero-alloc)
// Cons: Limited utilities (no WhenAll, WhenAny, no channel/queue)
async Awaitable DoWorkAwaitable()
{
await Awaitable.NextFrameAsync(destroyCancellationToken);
await Awaitable.WaitForSecondsAsync(1f, destroyCancellationToken);
}
// === Option B: UniTask (third-party: com.cysharp.unitask) ===
// Pros: Rich API (WhenAll, WhenAny, channels), PlayerLoop integration, zero-alloc
// Cons: External dependency, must learn UniTask-specific patterns
async UniTask DoWorkUniTask()
{
await UniTask.NextFrame(cancellationToken: destroyCancellationToken);
await UniTask.Delay(1000, cancellationToken: destroyCancellationToken);
// UniTask extras: WhenAll, WhenAny, Channel, AsyncReactiveProperty
}
// Converting between them (if mixing is unavoidable):
// Awaitable -> UniTask: not directly; use .AsTask() as bridge
// UniTask -> Awaitable: not directly; use .AsTask() as bridge
```
GOTCHA: Awaitable is built into Unity 6+ and requires no packages. UniTask (com.cysharp.unitask) is a mature third-party library with richer functionality. Do NOT mix both in the same codebase without a clear boundary -- their cancellation patterns, pooling behavior, and PlayerLoop integration differ. If targeting Unity 6+, Awaitable covers most needs. Use UniTask if you need advanced patterns like `WhenAll`, async LINQ, or `IUniTaskAsyncEnumerable`.
---
## Anti-Patterns Quick Reference
| Anti-Pattern | Problem | Fix |
|---|---|---|
| `await Task.Delay()` in Unity | Ignores TimeScale, no frame sync | Use `Awaitable.WaitForSecondsAsync()` |
| `Task.Run()` for Unity computation | Thread pool with no main thread return | Use `Awaitable.BackgroundThreadAsync()` + `MainThreadAsync()` |
| `StopAllCoroutines()` as cleanup | Nuclear option; stops coroutines you didn't start | Track and stop specific coroutines |
| Ignoring return value of `StartCoroutine` | Cannot cancel later | Store the `Coroutine` reference |
| `yield return new WaitForSeconds(0)` | Unclear intent, allocates | Use `yield return null` (no allocation) |
| `async Task` methods in MonoBehaviour | Task exceptions lost, no destroyCancellationToken integration | Use `async Awaitable` |
## Related Skills
- **unity-scripting** -- Coroutine fundamentals, Awaitable API reference, yield types
- **unity-lifecycle** -- destroyCancellationToken, object destruction timing
- **unity-performance** -- Async profiling, allocation tracking
## Additional Resources
- [Awaitable API](https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Awaitable.html)
- [Coroutines Manual](https://docs.unity3d.com/6000.3/Documentation/Manual/Coroutines.html)
- [Addressables AsyncOperationHandle](https://docs.unity3d.com/Packages/com.unity.addressables@2.3/manual/index.html)
- [UniTask GitHub](https://github.com/Cysharp/UniTask)More Accessibility skills
ui-ux-pro-max
nextlevelbuilder/ui-ux-pro-max-skill
UI/UX design intelligence for web, mobile, and desktop. This skill should be used when designing, building, reviewing, or fixing interfaces, including pages, components, design systems, accessibility, interaction, responsive layout, typography, color, charts, and stack-specific UI implementation. Searchable local data: 79 searchable styles (50 active), 192 product palettes and reasoning profiles, 74 font pairings, 119 UX guidelines, 105 icons, 17 GSAP presets, 25 chart types, and 22 stacks.
hyperframes-core
heygen-com/hyperframes
The HyperFrames composition contract — build one renderable project. Use for composition structure, the `data-*` timing attributes, `class="clip"`, tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Also covers Tailwind projects and the STORYBOARD.md / SCRIPT.md plan formats. Read before writing composition HTML.
web-design-guidelines
vercel-labs/agent-skills
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".

