こんにちは、takuです。
UniTaskを使った非同期処理、便利ですよね。
自分もUnityのプロジェクトで日常的に使っているんですが、先日ハマった問題があります。
UniTaskCompletionSourceで例外をセットしたのに、なぜかtry-catchで捕まえられない。
デバッガーを見ると、例外が汎用的な場所に飛んで行ってしまって、本来意図した場所でキャッチできていませんでした。
なぜ例外がキャッチできなくなるのか
結論から言うと、UniTaskCompletionSourceで例外をセットした際に、その結果を誰もawaitしていないと、例外はUnityのグローバルな例外ハンドラに飛んでいってしまうんです。
つまり、例外をセットしただけでは不十分で、必ずその結果を誰かがawaitして受け取る必要があります。
こういうコードを書いたことはないでしょうか。
using Cysharp.Threading.Tasks;
using UnityEngine;
public class BadExample : MonoBehaviour
{
private UniTaskCompletionSource<int> _completionSource;
async void Start()
{
_completionSource = new UniTaskCompletionSource<int>();
// 例外を発生させるメソッドを呼ぶが、awaitしない
ProcessAsync();
// 例外をセット
_completionSource.TrySetException(new System.Exception("何かエラーが起きた"));
Debug.Log("このログは表示される");
}
async UniTask ProcessAsync()
{
try
{
// ここでCompletionSourceの結果を待つ
var result = await _completionSource.Task;
Debug.Log($"結果: {result}");
}
catch (System.Exception ex)
{
// 本来ここで捕まえたい
Debug.LogError($"例外をキャッチ: {ex.Message}");
}
}
}このコードの問題点は、ProcessAsync()を呼び出しているのに、その結果をawaitしていないことです。
一見すると、ProcessAsyncの中でtry-catchを書いているので、例外は捕まえられそうに見えます。
でも実際には、ProcessAsyncが開始された後、呼び出し元のStart()は先に進んでしまいます。
そして_completionSource.TrySetExceptionが実行されたとき、もしProcessAsyncの実行が何らかの理由で中断されていたり、await前に処理が止まっていたりすると、例外は誰にも受け取られません。
結果として、例外はグローバルな場所に飛んで行ってしまい、意図したtry-catchブロックでは捕まえられないんです。
正しい実装パターン
では、どう書けばいいのか。
答えはシンプルで、UniTaskを返すメソッドは必ずawaitするか、明示的に結果を待つ仕組みを作ることです。
using Cysharp.Threading.Tasks;
using UnityEngine;
public class GoodExample : MonoBehaviour
{
private UniTaskCompletionSource<int> _completionSource;
async UniTaskVoid Start()
{
_completionSource = new UniTaskCompletionSource<int>();
// awaitして結果を待つ
await ProcessAsync();
Debug.Log("処理完了");
}
async UniTask ProcessAsync()
{
try
{
// 別のタイミングで例外をセット
TriggerExceptionLater();
// ここでCompletionSourceの結果を待つ
var result = await _completionSource.Task;
Debug.Log($"結果: {result}");
}
catch (System.Exception ex)
{
// ちゃんとここで捕まえられる
Debug.LogError($"例外をキャッチ: {ex.Message}");
}
}
void TriggerExceptionLater()
{
_completionSource.TrySetException(new System.Exception("何かエラーが起きた"));
}
}このコードでは、ProcessAsync()を呼び出す側でawaitしているため、例外が発生してもちゃんとtry-catchで捕まえられます。
もう一つのポイントは、StartメソッドをUniTaskVoidにしていることです。
async voidではなくasync UniTaskVoidにすることで、UniTaskのコンテキストで実行され、例外処理がより安定します。
まとめ:awaitしてこその例外処理
UniTaskCompletionSourceは強力なツールですが、例外処理には注意が必要です。
例外をセットしても、その結果を誰もawaitしていなければ、意図した場所でキャッチできません。
自分がこの問題にハマったときは、「なんで例外が消えるんだ?」と悩みました。
結局、awaitを忘れていたことが原因でした。
UniTaskを使うときは、常に「この非同期処理の結果を誰が待つのか」「例外が起きたときにどこで処理するのか」を意識すると、こういう問題は防げるんじゃないかなと思います。
特に大規模なプロジェクトでは、非同期処理が複雑に絡み合うので、一つひとつの処理でちゃんとawaitして例外を受け取る設計が大事だなと感じています。
みなさんも、UniTaskを使うときは例外処理の流れを意識してみてください。
それでは。