惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
GRAHAM CLULEY
V
V2EX
WordPress大学
WordPress大学
博客园 - Franky
Last Week in AI
Last Week in AI
博客园 - 司徒正美
有赞技术团队
有赞技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 【当耐特】
V
Visual Studio Blog
C
CERT Recently Published Vulnerability Notes
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Jina AI
Jina AI
Attack and Defense Labs
Attack and Defense Labs
腾讯CDC
The Hacker News
The Hacker News
Hugging Face - Blog
Hugging Face - Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
J
Java Code Geeks
人人都是产品经理
人人都是产品经理
阮一峰的网络日志
阮一峰的网络日志
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
A
Arctic Wolf
量子位
博客园 - 聂微东
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
News and Events Feed by Topic
雷峰网
雷峰网
博客园_首页
Google Online Security Blog
Google Online Security Blog
Spread Privacy
Spread Privacy
罗磊的独立博客
H
Hacker News: Front Page
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
TaoSecurity Blog
TaoSecurity Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
IT之家
IT之家
The Cloudflare Blog
爱范儿
爱范儿
博客园 - 叶小钗
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Apple Machine Learning Research
Apple Machine Learning Research
酷 壳 – CoolShell
酷 壳 – CoolShell

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Do We Still Need UniTask in the Unity 6 Era? Choosing Between Awaitable and UniTask
GameDevToolLab · 2026-06-25 · via DEV Community

Introduction

When writing asynchronous code in Unity, Coroutine and UniTask have long been the practical choices.

A major reason is that many Unity APIs must be called from the main thread.

If you simply use .NET Task and move work to a background thread, you can run into problems the moment you touch objects such as GameObject, Transform, or SceneManager.

UniTask has been a very convenient solution to this constraint.

await UniTask.SwitchToThreadPool();

// Heavy computation, JSON parsing, binary parsing, etc.
var result = HeavyWork();

await UniTask.SwitchToMainThread();

// Touch Unity APIs
gameObject.name = result.Name;

However, starting with Unity 2023.1, Unity introduced the standard Awaitable API.

In Unity 6, APIs such as Awaitable.NextFrameAsync(), Awaitable.WaitForSecondsAsync(), Awaitable.BackgroundThreadAsync(), and Awaitable.MainThreadAsync() are available.

In other words, Unity now provides a standard way to write a certain amount of async/await code.

So, in the Unity 6 era, do we still need UniTask?

This article is not a basic UniTask tutorial. Instead, it focuses on what changed with the arrival of Awaitable, and where UniTask still remains valuable.


Conclusion

Here is the conclusion first.

With the arrival of Awaitable, Unity is no longer in a world where "UniTask is the only realistic way to use async/await."

However, if you want to operate asynchronous workflows properly in an actual game application, UniTask is still very useful.

A rough breakdown looks like this:

Use case Recommended choice
Waiting until the next frame Awaitable is often enough
Waiting for a few seconds Awaitable is often enough
Lightweight coroutine replacement Awaitable is often enough
Library code where you want to avoid external dependencies Returning Awaitable can be a good fit
Waiting for multiple async operations together UniTask is stronger
Large-scale loading, networking, Addressables control UniTask is stronger
Practical cancellation handling UniTask is stronger
Fine-grained PlayerLoopTiming control UniTask is stronger
Tracking leaked or forgotten async operations UniTask is stronger

Very roughly:

Small async code: Awaitable
Application-wide async infrastructure: UniTask

That is the mental model I use.


What changed with Awaitable?

The biggest change is that Unity now supports code like this as a standard feature:

await Awaitable.BackgroundThreadAsync();

// Now on a ThreadPool thread
var result = HeavyWork();

await Awaitable.MainThreadAsync();

// Back on the Unity main thread
gameObject.name = result.Name;

This is a big deal.

Previously, even if you wanted to use async/await in Unity, using .NET Task directly often did not fit well with Unity's main-thread restrictions.

You can move work to a background thread with Task.Run.

var result = await Task.Run(() =>
{
    return HeavyWork();
});

However, touching Unity APIs inside that code is dangerous.

var result = await Task.Run(() =>
{
    // NG
    return transform.position;
});

Many Unity APIs are designed to be used from the main thread.

Because of that, you need a design that explicitly returns to the main thread before touching Unity APIs.

UniTask made this very natural.

await UniTask.SwitchToThreadPool();

var result = HeavyWork();

await UniTask.SwitchToMainThread();

ApplyResultToUnityObject(result);

Today, Unity's standard Awaitable API can express the same basic idea.

await Awaitable.BackgroundThreadAsync();

var result = HeavyWork();

await Awaitable.MainThreadAsync();

ApplyResultToUnityObject(result);

This means that introducing UniTask only to switch back to the main thread is less necessary than it used to be.


Task and Awaitable also differ in continuation timing

The arrival of Awaitable did not just add more APIs that can be used with async/await.

.NET Task and Unity's Awaitable also differ in how continuations are handled.

In Unity, a continuation from a Task called on the main thread can be posted to UnitySynchronizationContext, and may resume on the next Update tick.

On the other hand, Unity's Awaitable is designed to run continuations synchronously when the operation completes.

That means that when dealing with Unity-provided asynchronous operations, Awaitable is designed to avoid unnecessary one-frame delays more easily.

This does not mean "Awaitable is always faster."

But it does mean that Awaitable is designed around Unity's frame loop, while Task is a general-purpose .NET abstraction.

That distinction is worth keeping in mind.


await is not magic that automatically moves work to another thread

This is an easy misunderstanding.

Writing await does not automatically move the entire operation to a background thread.

await Awaitable.BackgroundThreadAsync();

This means that the following continuation resumes on a background thread.

Conversely:

await Awaitable.MainThreadAsync();

This means that the following continuation resumes on the Unity main thread.

So the following code is dangerous:

await Awaitable.BackgroundThreadAsync();

// NG: touching Unity APIs from a background thread
gameObject.name = "Loaded";

The correct pattern is to return to the main thread before touching Unity APIs.

await Awaitable.BackgroundThreadAsync();

var result = HeavyWork();

await Awaitable.MainThreadAsync();

gameObject.name = result.Name;

The same idea applies to UniTask.

await UniTask.SwitchToThreadPool();

var result = HeavyWork();

await UniTask.SwitchToMainThread();

gameObject.name = result.Name;

Awaitable does not remove Unity's main-thread restriction.

What changed is that Unity now provides standard APIs to work with that restriction.


Awaitable is very convenient as a coroutine replacement

Awaitable can replace many common coroutine patterns quite naturally.

Waiting until the next frame:

await Awaitable.NextFrameAsync();

Waiting for a fixed amount of time:

await Awaitable.WaitForSecondsAsync(1.0f);

Waiting for the FixedUpdate timing:

await Awaitable.FixedUpdateAsync();

Waiting until the end of the frame:

await Awaitable.EndOfFrameAsync();

For lightweight use cases, this is often enough.

For example, UI effects, simple waits, short samples, and small libraries that want to avoid external dependencies can often use Awaitable without bringing in UniTask.

private async Awaitable ShowMessageAsync(string message)
{
    label.text = message;
    await Awaitable.WaitForSecondsAsync(1.0f);
    label.text = "";
}

For this level of code, UniTask is not mandatory.


What Awaitable replaced, and what it did not

Awaitable did not erase all of UniTask's value.

However, some of UniTask's previous value became less unique.

For example:

Writing next-frame waits with async/await
Writing WaitForSeconds-like waits with async/await
Writing EndOfFrame-like waits with async/await
Returning to the main thread using Unity-standard APIs
Moving to a background thread using Unity-standard APIs

These can now be written fairly naturally with Awaitable.

On the other hand, UniTask still has strong value in areas such as:

WhenAll / WhenAny / WhenEach
Fine-grained PlayerLoopTiming control
Frame-based APIs such as DelayFrame
Unity-oriented CancellationToken workflows
UniTaskTracker
Integration with Addressables, DOTween, uGUI, and other surrounding libraries

So the value that became less unique is:

Making async/await usable in Unity at all

The value that remains is:

Making asynchronous workflows easier to operate across a full application

Once you separate these two ideas, the decision becomes much clearer.


So where is UniTask still strong?

If Awaitable can switch threads, does that mean UniTask lost its value?

Personally, I see it less as "UniTask lost value" and more as "UniTask's role shifted."

Previously, UniTask often served as:

The practical foundation for using async/await in Unity

After Awaitable, its role is closer to:

A practical foundation for operating larger asynchronous workflows in Unity

The difference becomes clear in the following areas.


UniTask strength 1: WhenAll / WhenAny / WhenEach are practical

In real loading code, a single async operation is rarely the whole story.

You often want to start multiple operations, such as loading master data, Addressables assets, textures, or network responses, and then wait for all of them.

var playerTask = LoadPlayerAsync();
var itemTask = LoadItemAsync();
var questTask = LoadQuestAsync();

await UniTask.WhenAll(playerTask, itemTask, questTask);

Receiving multiple results is also clean.

var (player, item, quest) = await UniTask.WhenAll(
    LoadPlayerAsync(),
    LoadItemAsync(),
    LoadQuestAsync()
);

This is very useful in practice.

If you await each operation one by one, they become sequential.

var player = await LoadPlayerAsync();
var item = await LoadItemAsync();
var quest = await LoadQuestAsync();

In this form, LoadItemAsync does not start until LoadPlayerAsync has finished.

If you start all operations first and then wait for them together, operations that can run concurrently will proceed concurrently.

var playerTask = LoadPlayerAsync();
var itemTask = LoadItemAsync();
var questTask = LoadQuestAsync();

var (player, item, quest) = await UniTask.WhenAll(
    playerTask,
    itemTask,
    questTask
);

In game loading code, this can directly affect loading time.

If you want to process results as they complete, WhenEach is also useful.

var tasks = new[]
{
    LoadMasterAsync("player"),
    LoadMasterAsync("item"),
    LoadMasterAsync("quest"),
};

await foreach (var result in UniTask.WhenEach(tasks))
{
    var master = result.GetResult();
    ApplyMaster(master);
}

WhenEach is useful when you want to apply results in completion order instead of waiting for everything first.

If you want to handle errors individually, you can inspect each result.

await foreach (var result in UniTask.WhenEach(tasks))
{
    if (result.IsFaulted)
    {
        Debug.LogException(result.Exception);
        continue;
    }

    ApplyMaster(result.GetResult());
}

This is where Awaitable's characteristics matter.

Awaitable instances are pooled internally, and you should not await the same Awaitable instance more than once.

Awaiting the same instance multiple times can lead to exceptions, deadlocks, or behavior close to undefined behavior.

Also, standard Awaitable does not provide the same level of composition APIs as UniTask's WhenAll, WhenAny, and WhenEach.

So Awaitable is convenient for lightweight coroutine replacement, but UniTask is more practical when you need to manage many loading operations or network requests together.


UniTask strength 2: Fine-grained PlayerLoopTiming control

In Unity async code, simply returning to the main thread is not always enough.

Sometimes you need to care about where in the PlayerLoop you resume.

For example:

Resume before Update
Resume after LateUpdate
Run at FixedUpdate timing
Run at an EndOfFrame-like timing

UniTask lets you specify PlayerLoopTiming.

await UniTask.Yield(PlayerLoopTiming.Update);
await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate);

You can also specify timing when switching back to the main thread.

await UniTask.SwitchToMainThread(PlayerLoopTiming.PreLateUpdate);

This can matter for code such as:

UI updates
Camera post-processing
Code that depends on Transform updates
Loading progress display
Screenshots at the end of the frame
Post-processing after Addressables or AsyncOperation completion

Awaitable has NextFrameAsync, FixedUpdateAsync, and EndOfFrameAsync.

However, it does not expose PlayerLoopTiming as finely as UniTask.

UniTask's PlayerLoopTiming includes many phases such as Initialization, EarlyUpdate, FixedUpdate, PreUpdate, Update, PreLateUpdate, PostLateUpdate, and LastPostLateUpdate.

So the practical distinction is:

If returning to the main thread is enough: Awaitable can be enough
If the exact PlayerLoop timing matters: UniTask is stronger


UniTask strength 3: Yield and NextFrame can be used intentionally

A subtle but important UniTask detail is the difference between Yield and NextFrame.

await UniTask.Yield();

and:

await UniTask.NextFrame();

do not mean exactly the same thing.

If your intent is "wait until the next frame" in the same sense as yield return null, then UniTask.NextFrame() usually expresses that intent more clearly.

await UniTask.NextFrame();

On the other hand, UniTask.Yield() resumes at the specified PlayerLoopTiming.

Depending on when it is called, it may resume within the same frame.

So if you casually use Yield as a replacement for yield return null, it can cause bugs in code that depends on frame boundaries.

This is easy to miss in basic tutorials.

In asynchronous code, it is not enough to say "wait."

Sometimes it matters when the continuation resumes.


UniTask strength 4: Cancellation is easier to operate in practice

In games, an async operation often outlives the object that started it.

For example:

The scene changed
The screen was closed
The prefab was destroyed
A network request was canceled
The player returned to the title screen during loading

If the async operation keeps running, it may later try to touch an object that has already been destroyed.

UniTask makes common Unity cancellation patterns easy to write.

private async UniTaskVoid Start()
{
    var token = this.GetCancellationTokenOnDestroy();

    try
    {
        await LoadAsync(token);
    }
    catch (OperationCanceledException)
    {
        // Cancellation caused by Destroy, screen transition, etc.
    }
}

private async UniTask LoadAsync(CancellationToken cancellationToken)
{
    await UniTask.Delay(1000, cancellationToken: cancellationToken);

    cancellationToken.ThrowIfCancellationRequested();

    // Touch Unity APIs
    label.text = "Loaded";
}

Awaitable APIs can also accept CancellationToken.

However, UniTask provides many practical patterns that fit Unity development very well.

In particular, GetCancellationTokenOnDestroy() makes it easy to tie the lifetime of an async operation to the lifetime of a MonoBehaviour.


UniTask strength 5: UniTaskTracker helps find forgotten operations

Asynchronous operations are easy to leave behind invisibly.

For example:

SomeAsync().Forget();

Forget() is convenient, but if used carelessly, the operation may continue running even after the screen has closed.

Loading, networking, polling, and UI effects can keep running and produce mysterious logs or exceptions later.

UniTask includes UniTaskTracker.

This is very useful in practice.

Async problems are not always about the speed of a single operation.

Often the real issue is:

An operation that should have ended is still running
An operation that should have been canceled is still active
The same async operation was started multiple times

Awaitable does not have an equivalent tracker.

This is another reason UniTask remains strong as an application-wide async foundation.


A common allocation trap around Progress

Even if you use UniTask, surrounding code can still allocate.

A typical example is Progress<T>.

var progress = new Progress<float>(x =>
{
    progressBar.value = x;
});

This is convenient, but if created frequently, it can become a source of allocations.

UniTask provides Progress.Create.

var progress = Progress.Create<float>(x =>
{
    progressBar.value = x;
});

For very high-frequency progress reporting, you can also implement IProgress<T> yourself and reuse the instance.

public sealed class ProgressBarReporter : IProgress<float>
{
    private readonly UnityEngine.UI.Slider slider;

    public ProgressBarReporter(UnityEngine.UI.Slider slider)
    {
        this.slider = slider;
    }

    public void Report(float value)
    {
        slider.value = value;
    }
}

var progress = new ProgressBarReporter(progressBar);
await LoadAsync(progress, cancellationToken);

When you see allocations while using UniTask, the cause may not be UniTask itself.

It may be Progress, lambdas, closures, LINQ, array allocations, or other surrounding code.

So it is important to look at the whole implementation, not just whether UniTask is being used.


Moving to the ThreadPool does not automatically make loading faster

This is another common misunderstanding.

await Awaitable.BackgroundThreadAsync();

or:

await UniTask.SwitchToThreadPool();

can move heavy work to a background thread.

However, Unity loading itself does not necessarily become fully background-threaded just because you moved your continuation.

For example, Addressables, AssetBundle loading, scene loading, texture creation, and mesh creation may still include main-thread work internally.

So you should not assume:

I moved to BackgroundThread, so all Unity loading no longer blocks the main thread.

Moving to a background thread is most effective for work that does not depend on Unity APIs.

For example:

JSON parsing
MessagePack parsing
Custom binary parsing
Encryption / decryption
Compression / decompression
Hash calculation
Large array processing
Pure computation such as pathfinding or scoring

On the other hand, creating UnityEngine.Object, touching Transform, or loading scenes still requires main-thread work at some point.

This boundary matters.

Also, if you target WebGL or web builds, be careful with designs that depend on ThreadPool.

The UniTask README explains that APIs such as UniTask.SwitchToThreadPool() and UniTask.RunOnThreadPool() do not work on WebGL and similar environments.

If web builds are part of your target, avoid depending too heavily on ThreadPool-based designs. Instead, consider splitting work across frames, using coroutine or PlayerLoop-based distribution, or checking the current state of Job System support for your target.


A safer pattern for using ThreadPool with Awaitable

When using Awaitable for background work, pay attention to which thread the method completes on.

For example, the following code needs caution:

private async Awaitable<ParsedData> ParseAsync(byte[] bytes)
{
    await Awaitable.BackgroundThreadAsync();

    var parsed = ParseBytes(bytes);

    return parsed;
}

This method does its work on a background thread and then completes there.

If the caller immediately touches Unity APIs after awaiting it, that can be dangerous.

var data = await ParseAsync(bytes);

// If ParseAsync completes on a background thread,
// touching Unity APIs here is dangerous.
gameObject.name = data.Name;

If the caller may touch Unity APIs after this method returns, a safer default is to return to the main thread before completing the method.

private async Awaitable<ParsedData> ParseAsync(
    byte[] bytes,
    bool continueOnMainThread = true)
{
    await Awaitable.BackgroundThreadAsync();

    var parsed = ParseBytes(bytes);

    if (continueOnMainThread)
    {
        await Awaitable.MainThreadAsync();
    }

    return parsed;
}

With this pattern, normal calls complete after returning to the main thread.

var data = await ParseAsync(bytes);

// ParseAsync has returned to the main thread before completion,
// so touching Unity APIs is easier to reason about.
gameObject.name = data.Name;

If you want to chain multiple heavy operations on a background thread, you can opt out explicitly.

var data = await ParseAsync(bytes, continueOnMainThread: false);

// If you want to touch Unity APIs here,
// explicitly return to the main thread.
await Awaitable.MainThreadAsync();

gameObject.name = data.Name;

The important point is to make the contract clear:

Which thread does this async method complete on?
Is it safe to touch Unity APIs after awaiting it?
How far does cancellation propagate?

Also, Unity does not necessarily stop background operations automatically when exiting Play Mode.

When using background work in the Editor, consider using Application.exitCancellationToken so the operation can be canceled when Play Mode ends.


Using RunOnThreadPool with UniTask

With UniTask, you can also write background execution that returns a result like this:

var parsed = await UniTask.RunOnThreadPool(() =>
{
    return ParseBytes(bytes);
});

ApplyToUnityObject(parsed);

The same warning applies here.

Do not touch Unity APIs inside ParseBytes.

var parsed = await UniTask.RunOnThreadPool(() =>
{
    // NG
    var position = transform.position;

    return ParseBytes(bytes);
});

Code running on the ThreadPool should be limited to pure work that does not depend on Unity APIs.


For library development, returning Awaitable can make sense

For application code, UniTask is still very useful.

However, for libraries or Unity Asset Store packages, you may want to avoid adding external dependencies.

If you can target Unity 6 or later, returning Awaitable can be a realistic choice.

public async Awaitable LoadAsync(CancellationToken cancellationToken)
{
    await Awaitable.NextFrameAsync(cancellationToken);

    // Work
}

If the application uses UniTask, it can adapt Awaitable as needed.

Library side: return Awaitable
Application side: convert or wrap it for UniTask-based operation

This keeps the library dependency-light while still allowing the application to use UniTask's operational features.

UniTask can handle Awaitable with AsUniTask(), so the two do not need to be treated as mutually exclusive.


Practical guideline

Here is a practical rule of thumb.

Case Awaitable UniTask
Small waiting logic Good fit Also possible
Lightweight coroutine replacement Good fit Also possible
Standard Unity-only implementation Good fit Adds external dependency
Library return type Good fit Requires users to depend on UniTask
Waiting for multiple loads together Weak Good fit
Managing many async operations Weak Good fit
Designing cancellation across systems Possible Good fit
Fine-grained PlayerLoopTiming control Weak Good fit
Finding forgotten async operations Weak Good fit
Considering WebGL / ThreadPool limitations Be careful Be careful

The important point is that Awaitable and UniTask are not enemies.

Awaitable is Unity's lightweight standard async API.

UniTask is a practical tool for managing larger async workflows in Unity applications.


Summary

Awaitable made asynchronous programming in Unity much more practical.

The biggest changes are that Unity now provides standard APIs for:

Waiting until the next frame
Waiting for seconds
Waiting for FixedUpdate
Waiting until EndOfFrame
Awaiting AsyncOperation
Switching to the main thread
Switching to a background thread

Because of this, simple coroutine replacements and small async workflows can often be written with Awaitable alone.

However, real game development rarely stops at single async operations.

You often need to handle:

Large-scale loading
Networking
Addressables
Cancellation
Screen transitions
Destroy-linked lifetimes
Exception handling
Progress reporting
PlayerLoopTiming control
Tracking forgotten operations

Once you consider these operational concerns, UniTask still has a strong reason to exist.

The conclusion is:

Awaitable made Unity-standard async/await much more realistic.
But if you need to operate asynchronous workflows in a real application, UniTask is still powerful.

In the Unity 6 era, UniTask is no longer just a coroutine replacement.

It is better understood as a foundation for organizing asynchronous workflows across an application.


References