Skip to main content
Unity 2023.1+ · Free for indie

Async/await
AAAAAAA AAAAAAAA

Struct-based tasks. Source-generated cancellation. Burst & ECS ready. Zero allocation on the happy path.

EnemyAI.cs
public partial class EnemyAI : MonoBehaviour
{
async ValkarnTask ChasePlayer()
{
while (true)
{
MoveTowards(player.position);
await ValkarnTask.Yield(); // auto-cancels on Destroy
}
}
}

var (tex, sfx, data) = await ValkarnTask.WhenAll(
LoadTextureAsync(),
LoadAudioAsync(),
FetchDataAsync());
0allocs happy path
0PlayerLoop timings
0analyzer rules
0xfaster pool vs UniTask

Built for Unity's execution model.

Not a port of .NET patterns — every decision is Unity-first.

Zero Allocation

Struct-based ValkarnTask avoids heap pressure on every success path. Completed tasks are free.

🔄Auto-Cancel on Destroy

Mark the class partial. The source generator binds cancellation to the MonoBehaviour lifetime.

🧵Thread-Aware Pool

Lock-free CAS on the main thread, Treiber stack on worker threads. No unnecessary atomics.

🎯16 PlayerLoop Timings

From Initialization to TimeUpdate — precise control over when continuations resume.

📡Async Channels

Bounded and unbounded producer/consumer queues. WriteAsync, ReadAsync, TryRead — zero-alloc.

🚀Burst & ECS Ready

NativeTimerHeap, BurstSchedulerRunner, async ECS systems. First-class Unity DOTS support.

🔍17 Analyzer Rules

Zombie loops, mixed lifetimes, unawaited tasks — caught at compile time, not in production.

🛡️IL2CPP Safe

Explicit generics, no runtime reflection, link.xml stripping protection. Ships to consoles.

Feature comparison.

🟢 = best in row · ✦ = unique to Valkarn Tasks · ⓘ = hover for detail

FeatureSystem.TaskUniTaskAwaitableValkarn Tasks
Allocation on successDoes awaiting a completed task allocate?Yes — Task<T> is a classEvery async method returning Task<T> allocates a heap object, even when it returns synchronously — a constant GC tax in a 60 Hz game loop.No (struct)No (struct)No (struct)
Allocation on failureException / cancellation pathsYesYesUniTask advertises "~Zero allocation" — the tilde matters. Exceptions and cancellations still allocate, same as everyone else.YesYes
Auto-cancel on DestroyTied to MonoBehaviour lifetimeManualManualManualSource-gen ✦Mark the class partial. A source generator wires a CancellationToken to OnDestroy — no boilerplate field, no OnEnable/OnDisable, no forgotten cancellation.
PlayerLoop timingsScheduling precision1 (ThreadPool)Continuations run on the .NET ThreadPool. Moving back to the main thread requires explicit marshalling via UnitySynchronizationContext.16UniTask and Valkarn Tasks both implement the full set of 16 PlayerLoop timings, from Initialization through TimeUpdate.6Awaitable exposes 6 hooks: NextFrameAsync, FixedUpdateAsync, EndOfFrameAsync, WaitForSecondsAsync, MainThreadAsync, BackgroundThreadAsync — not the full PlayerLoop.16
ECS / Entities compatibilityWorks alongside Unity DOTSN/A⚠ BreaksUnity's Entities package resets the PlayerLoop on initialization, which clears UniTask's registered runners. Any task scheduled before that point is silently lost.PartialAwaitable works in ECS systems but has no NativeTimerHeap, no BurstSchedulerRunner, and no async ECS system helpers.Full ✦Full DOTS support: NativeTimerHeap for Burst-compatible scheduling, BurstSchedulerRunner, AsyncSystemUtilities for ECS systems, JobHandle bridge.
Double-await safetyAwaiting the same task twice✓ Safe✗ UndefinedAwaiting a UniTask more than once is explicitly undefined behavior. In practice it causes a deadlock or corrupts pool state.✗ DeadlockAwaiting an Awaitable twice deadlocks or throws, depending on whether it has already completed. There is no guard.✓ SafeDouble-return guards on all source paths. The second await on a completed task returns the cached result immediately.
Compile-time diagnosticsRoslyn analyzer rules0 rules1 ruleUniTask ships one rule (UNITASK001): warns when you forget to await a UniTask return value. No detection of zombie loops, mixed lifetimes, or structural bugs.0 rules17 rules ✦Catches zombie loops, mixed lifetimes, unawaited tasks, double-awaits, improper fire-and-forget, missing auto-cancel markers — before the build ships.
Thread-aware poolLock-free on main threadN/AInterlocked (all threads)UniTask's pool uses Interlocked.CompareExchange on every Push and Pop — including the main thread where there is never any real contention. Unnecessary atomic overhead on every task completion.N/ACAS / Treiber ✦Lock-free CAS on the main thread (no atomic overhead where it's not needed). Treiber stack for background threads. Each context gets the correct algorithm.
Async channelsBuilt-in producer/consumerBCL (class-based)System.Threading.Channels is class-based and allocates. Not integrated with Unity's PlayerLoop — continuations run on the ThreadPool.Unbounded onlyUniTask ships an unbounded single-consumer channel only. No bounded capacity, no backpressure. Zero-alloc on reads, but limited API.Bounded + Unbounded ✦Both bounded (with capacity and backpressure) and unbounded channels. WriteAsync, ReadAsync, TryRead, TryWrite, TryPeek — all zero-alloc on the fast path.
WhenAll / WhenAny combinatorsParallel task coordinationReturns Task[]Task.WhenAll returns Task<T[]>, requiring index-based access. No tuple destructuring.✓ (arrays only)UniTask.WhenAll supports typed tuples up to arity 15. A strong feature.Tuple up to 8 ✦var (tex, sfx, data) = await ValkarnTask.WhenAll(...) — up to 8 typed results. WhenAny returns the first completed result with its index.
Silent exception swallowingUnhandled errors in async void / fire-and-forgetAppDomain.UnhandledExceptionUniTaskScheduler.UnobservedTaskException⚠ Unity 6 bugUnity 6 had a confirmed bug where exceptions thrown inside Awaitable continuations were silently swallowed with no log output. Fixed in Unity 6000.0.5 — earlier 6.x releases are affected.Configurable handler ✦ValkarnTaskSettings.UnobservedExceptionHandler is user-configurable. Default: logs to Debug.LogException so no exception is ever silent.

Why make the switch?

Concrete reasons, not marketing copy.

vsSystem.Task
  • Every await allocatesTask<T> is a class. Every async method allocates a heap object, even when it returns synchronously. In a 60 Hz game loop that runs thousands of async operations, you are paying a GC tax on every single frame.
  • ThreadPool by defaultContinuations resume on the .NET ThreadPool, not on Unity's main thread. Any interaction with GameObjects, Transforms, or Unity APIs requires explicit marshalling through UnitySynchronizationContext — error-prone and verbose.
  • No MonoBehaviour bindingNothing prevents a Task from continuing after its owning MonoBehaviour is destroyed. The result is phantom null-reference exceptions, state corruption, and bugs that only appear when scenes are unloaded under load.
  • No Unity diagnosticsThe .NET runtime has no concept of Unity's lifetime model. Zero Roslyn rules catch the patterns that break games: zombie loops, destroyed-object access, fire-and-forget misuse.
System.Task is the right tool for .NET servers. It is the wrong tool for a real-time game loop.
vsUniTask
  • ~Zero is not ZeroUniTask's headline claim is "~Zero allocation." The tilde is load-bearing. Exception paths and cancellation still allocate — same as everyone else. Valkarn Tasks makes the same trade-off honestly and adds more guarantees on top.
  • Manual cancellation — alwaysBinding a UniTask to a MonoBehaviour requires: a CancellationTokenSource field, an OnEnable to (re-)initialize it, an OnDestroy to cancel and dispose it, and threading the token through every async call. Valkarn Tasks source-generates this entire pattern from a single [AutoCancel] attribute.
  • Breaks with Unity EntitiesUnity's Entities package calls PlayerLoop.SetPlayerLoop() on initialization, which overwrites UniTask's registered runners. Any task in flight at that moment is silently discarded. There is no warning. Valkarn Tasks' ECS integration is specifically designed to survive PlayerLoop resets.
  • Interlocked on the main threadUniTask's pool calls Interlocked.CompareExchange on every Push and Pop — including the main thread where there is no actual contention. These are unnecessary atomic operations on the hottest code path in your game. Valkarn Tasks uses plain CAS on the main thread and a Treiber stack on worker threads.
  • One analyzer rule, no structural checksUniTask ships UNITASK001: forget-to-await detection. That's it. Zombie loops (a loop that never exits because cancellation is never checked), mixed-lifetime tasks, double-awaits — none of these are caught. Valkarn Tasks ships 17 rules that cover these structural patterns.
  • Last release: October 2024UniTask's GitHub shows no active development since October 2024. Unity 6, DOTS 1.x, and future editor versions bring breaking changes that an unmaintained package cannot track.
UniTask was state-of-the-art in 2020. Valkarn Tasks is built for 2025 and beyond.
vsAwaitable
  • 6 timings, not 16Unity's Awaitable exposes 6 scheduling hooks: NextFrameAsync, FixedUpdateAsync, EndOfFrameAsync, WaitForSecondsAsync, MainThreadAsync, and BackgroundThreadAsync. Valkarn Tasks maps every one of Unity's 16 PlayerLoop phases — PreUpdate, PostLateUpdate, TimeUpdate, and more — as first-class await points.
  • Cannot await the same task twiceAwaiting an Awaitable that has already completed either deadlocks or throws, depending on internal state. There is no guard. Valkarn Tasks has double-return protection on every source path: the second await always returns the cached result immediately.
  • Unity 6 silently swallows exceptionsA confirmed Unity 6 bug causes exceptions thrown inside Awaitable continuations to disappear with no log output, no stack trace, no crash. Fixed in 6000.0.5 — meaning every Unity 6.0 through 6.0.4 project is affected. Valkarn Tasks routes all unobserved exceptions through a configurable handler that defaults to Debug.LogException.
  • No WhenAll, WhenAny, or channelsAwaitable has no combinator API. Running three loads in parallel and collecting their results requires a manual state machine. Valkarn Tasks provides WhenAll with tuple destructuring up to arity 8, WhenAny, and both bounded and unbounded async channels.
  • No lifecycle bindingAwaitable provides no mechanism to tie a task's lifetime to a GameObject. Every cancellation token must be created, stored, threaded through calls, and disposed manually.
Awaitable is a thin scheduling hook. Valkarn Tasks is a complete async runtime.

Ship faster. Allocate less.

Free for individuals and studios under $1M/year revenue.
One line in your manifest. No account required.