Struct-based tasks. Source-generated cancellation. Burst & ECS ready. Zero allocation on the happy path.
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());
Not a port of .NET patterns — every decision is Unity-first.
Struct-based ValkarnTask avoids heap pressure on every success path. Completed tasks are free.
Mark the class partial. The source generator binds cancellation to the MonoBehaviour lifetime.
Lock-free CAS on the main thread, Treiber stack on worker threads. No unnecessary atomics.
From Initialization to TimeUpdate — precise control over when continuations resume.
Bounded and unbounded producer/consumer queues. WriteAsync, ReadAsync, TryRead — zero-alloc.
NativeTimerHeap, BurstSchedulerRunner, async ECS systems. First-class Unity DOTS support.
Zombie loops, mixed lifetimes, unawaited tasks — caught at compile time, not in production.
Explicit generics, no runtime reflection, link.xml stripping protection. Ships to consoles.
🟢 = best in row · ✦ = unique to Valkarn Tasks · ⓘ = hover for detail
| Feature | System.Task | UniTask | Awaitable | Valkarn Tasks |
|---|---|---|---|---|
| Allocation on successDoes awaiting a completed task allocate? | Yes — Task<T> is a classⓘEvery 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 paths | Yes | YesⓘUniTask advertises "~Zero allocation" — the tilde matters. Exceptions and cancellations still allocate, same as everyone else. | Yes | Yes |
| Auto-cancel on DestroyTied to MonoBehaviour lifetime | Manual | Manual | Manual | Source-gen ✦ⓘMark the class partial. A source generator wires a CancellationToken to OnDestroy — no boilerplate field, no OnEnable/OnDisable, no forgotten cancellation. |
| PlayerLoop timingsScheduling precision | 1 (ThreadPool)ⓘContinuations run on the .NET ThreadPool. Moving back to the main thread requires explicit marshalling via UnitySynchronizationContext. | 16ⓘUniTask and Valkarn Tasks both implement the full set of 16 PlayerLoop timings, from Initialization through TimeUpdate. | 6ⓘAwaitable exposes 6 hooks: NextFrameAsync, FixedUpdateAsync, EndOfFrameAsync, WaitForSecondsAsync, MainThreadAsync, BackgroundThreadAsync — not the full PlayerLoop. | 16 |
| ECS / Entities compatibilityWorks alongside Unity DOTS | N/A | ⚠ BreaksⓘUnity's Entities package resets the PlayerLoop on initialization, which clears UniTask's registered runners. Any task scheduled before that point is silently lost. | PartialⓘAwaitable 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 | ✗ UndefinedⓘAwaiting a UniTask more than once is explicitly undefined behavior. In practice it causes a deadlock or corrupts pool state. | ✗ DeadlockⓘAwaiting an Awaitable twice deadlocks or throws, depending on whether it has already completed. There is no guard. | ✓ SafeⓘDouble-return guards on all source paths. The second await on a completed task returns the cached result immediately. |
| Compile-time diagnosticsRoslyn analyzer rules | 0 rules | 1 ruleⓘUniTask 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 rules | 17 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 thread | N/A | Interlocked (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/A | CAS / 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/consumer | BCL (class-based)ⓘSystem.Threading.Channels is class-based and allocates. Not integrated with Unity's PlayerLoop — continuations run on the ThreadPool. | Unbounded onlyⓘUniTask 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 coordination | Returns 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-forget | AppDomain.UnhandledException | UniTaskScheduler.UnobservedTaskException | ⚠ Unity 6 bugⓘUnity 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. |
Concrete reasons, not marketing copy.
Free for individuals and studios under $1M/year revenue.
One line in your manifest. No account required.