例外処理 (タスク並列ライブラリ)

タスク内で実行中のユーザー コードによってスローされた、ハンドルされない例外は、呼び出し元のスレッドに反映されます。ただし、このトピックの後半で説明している特定の状況を除きます。 静的な、またはインスタンスの Task.Wait メソッドの 1 つを使用し、その呼び出しを try/catch ステートメント内に入れて例外を処理すると、例外が反映されます。 タスクが、アタッチされた子タスクの親である場合、または複数のタスクを待機している場合、複数の例外がスローされることがあります。

呼び出し元のスレッドにすべての例外を反映するために、Task インフラストラクチャが例外を AggregateException インスタンスにラップします。 AggregateException 例外には、スローされた元のすべての例外を調べるために列挙できる InnerExceptions プロパティがあり、個々に処理したり未処理にしたりできます。 また、 AggregateException.Handle メソッドを使用して元の例外を処理することもできます。

例外が 1 つだけスローされた場合でも、次の例のように AggregateException 例外にラップされます。


public static partial class Program
{
    public static void HandleThree()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                // Rethrow any other exception.
                else
                {
                    throw ex;
                }
            }
        }
    }
}
// The example displays the following output:
//        This exception is expected!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        Try
            task1.Wait()
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                ' Handle the custom exception.
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                    ' Rethrow any other exception.
                Else
                    Throw ex
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

ハンドルされない例外は、 AggregateException をキャッチして、内部例外を確認しないことで回避できます。 ただし、並列でない状況で基本的な Exception の種類をキャッチする場合と類似しているため、この操作はお勧めしません。 特定の操作を取得することなく例外をキャッチして回復しようとすると、プログラムが中間状態のままになるおそれがあります。

Task.Wait メソッドを呼び出してタスクの完了を待機する処理を行わない場合は、次の例のようにタスクの Exception プロパティから AggregateException 例外を取得することもできます。 詳細については、このトピックの「Task.Exception プロパティによる例外の確認」セクションを参照してください。


public static partial class Program
{
    public static void HandleFour()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        while (!task.IsCompleted) { }

        if (task.Status == TaskStatus.Faulted)
        {
            foreach (var ex in task.Exception?.InnerExceptions ?? new(Array.Empty<Exception>()))
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                // Rethrow any other exception.
                else
                {
                    throw ex;
                }
            }
        }
    }
}
// The example displays the following output:
//        This exception is expected!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        While Not task1.IsCompleted
        End While

        If task1.Status = TaskStatus.Faulted Then
            For Each ex In task1.Exception.InnerExceptions
                ' Handle the custom exception.
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                    ' Rethrow any other exception.
                Else
                    Throw ex
                End If
            Next
        End If
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

注意事項

前のコード例には、タスクの Task.IsCompleted プロパティをポーリングして、タスクがいつ完了したかを判断する while ループが含まれています。 これは非常に非効率的なので、運用環境のコードでは実行しないでください。

例外を反映するタスクを待機しない場合、またはタスクの Exception プロパティにアクセスする場合、例外はタスクがガベージ コレクトされるときに .NET の例外ポリシーに従ってエスカレートされます。

連結しているスレッドへ例外が上方向に通知されると、例外が発生した後も、タスクによって一部の項目の処理が続行される可能性があります。

注意

[マイ コードのみ] が有効になっている場合、Visual Studio では、例外をスローする行で処理が中断され、"ユーザー コードで処理されない例外" に関するエラー メッセージが表示されることがあります。このエラーは問題にはなりません。 F5 キーを押して続行し、以下の例に示す例外処理動作を確認できます。 Visual Studio による処理が最初のエラーで中断しないようにするには、 [ツール] メニューの [オプション]、[デバッグ] の順にクリックし、[全般] で [マイ コードのみを有効にする] チェック ボックスをオフにします。

アタッチされた子タスクと入れ子の AggregateExceptions

タスクに、例外をスローする子タスクがアタッチされている場合、その例外は親タスクに反映される前に AggregateException でラップされます。つまり、呼び出し元のスレッドに反映される前に、その例外が固有の AggregateException でラップされるということです。 このような場合、Task.WaitWaitAnyWaitAll の各メソッドでキャッチされた AggregateException 例外の InnerExceptions プロパティには、違反の原因となった元の例外ではなく、1 つ以上の AggregateException インスタンスが含まれます。 入れ子の AggregateException 例外を反復処理しなくて済むようにするには、Flatten メソッドを使用して入れ子の AggregateException をすべて削除します。これにより、AggregateException.InnerExceptions プロパティに元の例外が含まれるようになります。 次の例では、入れ子の AggregateException インスタンスが 1 つのループで平坦化され、処理されています。


public static partial class Program
{
    public static void FlattenTwo()
    {
        var task = Task.Factory.StartNew(() =>
        {
            var child = Task.Factory.StartNew(() =>
            {
                var grandChild = Task.Factory.StartNew(() =>
                {
                    // This exception is nested inside three AggregateExceptions.
                    throw new CustomException("Attached child2 faulted.");
                }, TaskCreationOptions.AttachedToParent);

                // This exception is nested inside two AggregateExceptions.
                throw new CustomException("Attached child1 faulted.");
            }, TaskCreationOptions.AttachedToParent);
        });

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.Flatten().InnerExceptions)
            {
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                else
                {
                    throw;
                }
            }
        }
    }
}
// The example displays the following output:
//    Attached child1 faulted.
//    Attached child2 faulted.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Factory.StartNew(Sub()
                                              Dim child1 = Task.Factory.StartNew(Sub()
                                                                                     Dim child2 = Task.Factory.StartNew(Sub()
                                                                                                                            Throw New CustomException("Attached child2 faulted.")
                                                                                                                        End Sub,
                                                                                                                        TaskCreationOptions.AttachedToParent)
                                                                                     Throw New CustomException("Attached child1 faulted.")
                                                                                 End Sub,
                                                                                 TaskCreationOptions.AttachedToParent)
                                          End Sub)

        Try
            task1.Wait()
        Catch ae As AggregateException
            For Each ex In ae.Flatten().InnerExceptions
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                Else
                    Throw
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       Attached child1 faulted.
'       Attached child2 faulted.

また、次の例のように、AggregateException.Flatten メソッドを使用して、単一の AggregateException インスタンス内の複数のタスクによってスローされた複数の AggregateException インスタンスから内部例外を再スローすることもできます。

public static partial class Program
{
    public static void TaskExceptionTwo()
    {
        try
        {
            ExecuteTasks();
        }
        catch (AggregateException ae)
        {
            foreach (var e in ae.InnerExceptions)
            {
                Console.WriteLine(
                    "{0}:\n   {1}", e.GetType().Name, e.Message);
            }
        }
    }

    static void ExecuteTasks()
    {
        // Assume this is a user-entered String.
        string path = @"C:\";
        List<Task> tasks = new();

        tasks.Add(Task.Run(() =>
        {
            // This should throw an UnauthorizedAccessException.
            return Directory.GetFiles(
                path, "*.txt",
                SearchOption.AllDirectories);
        }));

        tasks.Add(Task.Run(() =>
        {
            if (path == @"C:\")
            {
                throw new ArgumentException(
                    "The system root is not a valid path.");
            }
            return new string[] { ".txt", ".dll", ".exe", ".bin", ".dat" };
        }));

        tasks.Add(Task.Run(() =>
        {
            throw new NotImplementedException(
                "This operation has not been implemented.");
        }));

        try
        {
            Task.WaitAll(tasks.ToArray());
        }
        catch (AggregateException ae)
        {
            throw ae.Flatten();
        }
    }
}
// The example displays the following output:
//       UnauthorizedAccessException:
//          Access to the path 'C:\Documents and Settings' is denied.
//       ArgumentException:
//          The system root is not a valid path.
//       NotImplementedException:
//          This operation has not been implemented.
Imports System.Collections.Generic
Imports System.IO
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Try
            ExecuteTasks()
        Catch ae As AggregateException
            For Each e In ae.InnerExceptions
                Console.WriteLine("{0}:{2}   {1}", e.GetType().Name, e.Message,
                                  vbCrLf)
            Next
        End Try
    End Sub

    Sub ExecuteTasks()
        ' Assume this is a user-entered String.
        Dim path = "C:\"
        Dim tasks As New List(Of Task)

        tasks.Add(Task.Run(Function()
                               ' This should throw an UnauthorizedAccessException.
                               Return Directory.GetFiles(path, "*.txt",
                                                         SearchOption.AllDirectories)
                           End Function))

        tasks.Add(Task.Run(Function()
                               If path = "C:\" Then
                                   Throw New ArgumentException("The system root is not a valid path.")
                               End If
                               Return {".txt", ".dll", ".exe", ".bin", ".dat"}
                           End Function))

        tasks.Add(Task.Run(Sub()
                               Throw New NotImplementedException("This operation has not been implemented.")
                           End Sub))

        Try
            Task.WaitAll(tasks.ToArray)
        Catch ae As AggregateException
            Throw ae.Flatten()
        End Try
    End Sub
End Module
' The example displays the following output:
'       UnauthorizedAccessException:
'          Access to the path 'C:\Documents and Settings' is denied.
'       ArgumentException:
'          The system root is not a valid path.
'       NotImplementedException:
'          This operation has not been implemented.

デタッチされた子タスクの例外

既定では、子タスクはデタッチされた状態で作成されます。 デタッチされたタスクからスローされた例外は、処理されるか、直接の親に再スローされる必要があります。これらの例外は、アタッチされた子タスクが反映されるのとは異なる方法で、呼び出し元のスレッドに反映されます。 最上位の親では、デタッチされた子からの例外を手動で再スローすることで、 AggregateException にラップして、呼び出し元のスレッドに反映させることができます。


public static partial class Program
{
    public static void DetachedTwo()
    {
        var task = Task.Run(() =>
        {
            var nestedTask = Task.Run(
                () => throw new CustomException("Detached child task faulted."));

            // Here the exception will be escalated back to the calling thread.
            // We could use try/catch here to prevent that.
            nestedTask.Wait();
        });

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var e in ae.Flatten().InnerExceptions)
            {
                if (e is CustomException)
                {
                    Console.WriteLine(e.Message);
                }
            }
        }
    }
}
// The example displays the following output:
//    Detached child task faulted.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub()
                                 Dim nestedTask1 = Task.Run(Sub()
                                                                Throw New CustomException("Detached child task faulted.")
                                                            End Sub)
                                 ' Here the exception will be escalated back to joining thread.
                                 ' We could use try/catch here to prevent that.
                                 nestedTask1.Wait()
                             End Sub)

        Try
            task1.Wait()
        Catch ae As AggregateException
            For Each ex In ae.Flatten().InnerExceptions
                If TypeOf ex Is CustomException Then
                    ' Recover from the exception. Here we just
                    ' print the message for demonstration purposes.
                    Console.WriteLine(ex.Message)
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       Detached child task faulted.

子タスク内の例外を確認するために継続を使用する場合でも、親タスクによって例外を確認する必要があります。

他の処理との連携によるキャンセル処理を示す例外

タスク内のユーザー コードがキャンセル要求に応答する場合の正しいプロシージャは、その要求が伝えられたキャセル トークンに渡すために OperationCanceledException をスローすることです。 例外が反映される前に、タスク インスタンスによって、例外内のトークンがそのタスクが作成されたときに渡されたトークンと比較されます。 それらが同じである場合、タスクは TaskCanceledException でラップされた AggregateExceptionを反映します。これは、内部例外を調べると確認できます。 ただし、呼び出し元のスレッドがタスクを待機していない場合、このような特定の例外は反映されません。 詳細については、「タスクのキャンセル」をご覧ください。

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() =>
{
    CancellationToken ct = token;
    while (someCondition)
    {
        // Do some work...
        Thread.SpinWait(50_000);
        ct.ThrowIfCancellationRequested();
    }
},
token);

// No waiting required.
tokenSource.Dispose();
Dim someCondition As Boolean = True
Dim tokenSource = New CancellationTokenSource()
Dim token = tokenSource.Token

Dim task1 = Task.Factory.StartNew(Sub()
                                      Dim ct As CancellationToken = token
                                      While someCondition = True
                                          ' Do some work...
                                          Thread.SpinWait(500000)
                                          ct.ThrowIfCancellationRequested()
                                      End While
                                  End Sub,
                                  token)

内部例外をフィルター処理する Handle メソッドの使用

AggregateException.Handle メソッドを使用すると、"処理済み" として扱うことのできる例外をフィルターで除外できます。追加のロジックを使用する必要はありません。 AggregateException.Handle(Func<Exception,Boolean>) メソッドに用意されているユーザー デリゲートでは、例外の種類、その Message プロパティ、またはその例外に問題がないかどうかを判別できるその他の情報を調べることができます。 デリゲートが false を返す例外は、AggregateException メソッドが返された直後に新しい AggregateException.Handle インスタンスで再スローされます。

次の例は、機能的には、AggregateException.InnerExceptions コレクション内の各例外を調べるこのトピックの最初の例と同じです。 代わりに、この例外ハンドラーでは、例外ごとに AggregateException.Handle メソッドのオブジェクトを呼び出し、CustomException インスタンスでない例外のみを再スローしています。


public static partial class Program
{
    public static void HandleMethodThree()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            // Call the Handle method to handle the custom exception,
            // otherwise rethrow the exception.
            ae.Handle(ex =>
            {
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                return ex is CustomException;
            });
        }
    }
}
// The example displays the following output:
//        This exception is expected!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        Try
            task1.Wait()
        Catch ae As AggregateException
            ' Call the Handle method to handle the custom exception,
            ' otherwise rethrow the exception.
            ae.Handle(Function(e)
                          If TypeOf e Is CustomException Then
                              Console.WriteLine(e.Message)
                          End If
                          Return TypeOf e Is CustomException
                      End Function)
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

次の例はさらに複雑です。AggregateException.Handle メソッドを使用して、ファイルの列挙時に UnauthorizedAccessException 例外の特別な処理を行っています。

public static partial class Program
{
    public static void TaskException()
    {
        // This should throw an UnauthorizedAccessException.
        try
        {
            if (GetAllFiles(@"C:\") is { Length: > 0 } files)
            {
                foreach (var file in files)
                {
                    Console.WriteLine(file);
                }
            }
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
            {
                Console.WriteLine(
                    "{0}: {1}", ex.GetType().Name, ex.Message);
            }
        }
        Console.WriteLine();

        // This should throw an ArgumentException.
        try
        {
            foreach (var s in GetAllFiles(""))
            {
                Console.WriteLine(s);
            }
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
                Console.WriteLine(
                    "{0}: {1}", ex.GetType().Name, ex.Message);
        }
    }

    static string[] GetAllFiles(string path)
    {
        var task1 =
            Task.Run(() => Directory.GetFiles(
                path, "*.txt",
                SearchOption.AllDirectories));

        try
        {
            return task1.Result;
        }
        catch (AggregateException ae)
        {
            ae.Handle(x =>
            {
                // Handle an UnauthorizedAccessException
                if (x is UnauthorizedAccessException)
                {
                    Console.WriteLine(
                        "You do not have permission to access all folders in this path.");
                    Console.WriteLine(
                        "See your network administrator or try another path.");
                }
                return x is UnauthorizedAccessException;
            });
            return Array.Empty<string>();
        }
    }
}
// The example displays the following output:
//       You do not have permission to access all folders in this path.
//       See your network administrator or try another path.
//
//       ArgumentException: The path is not of a legal form.
Imports System.IO
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        ' This should throw an UnauthorizedAccessException.
        Try
            Dim files = GetAllFiles("C:\")
            If files IsNot Nothing Then
                For Each file In files
                    Console.WriteLine(file)
                Next
            End If
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message)
            Next
        End Try
        Console.WriteLine()

        ' This should throw an ArgumentException.
        Try
            For Each s In GetAllFiles("")
                Console.WriteLine(s)
            Next
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message)
            Next
        End Try
        Console.WriteLine()
    End Sub

    Function GetAllFiles(ByVal path As String) As String()
        Dim task1 = Task.Run(Function()
                                 Return Directory.GetFiles(path, "*.txt",
                                                           SearchOption.AllDirectories)
                             End Function)
        Try
            Return task1.Result
        Catch ae As AggregateException
            ae.Handle(Function(x)
                          ' Handle an UnauthorizedAccessException
                          If TypeOf x Is UnauthorizedAccessException Then
                              Console.WriteLine("You do not have permission to access all folders in this path.")
                              Console.WriteLine("See your network administrator or try another path.")
                          End If
                          Return TypeOf x Is UnauthorizedAccessException
                      End Function)
        End Try
        Return Array.Empty(Of String)()
    End Function
End Module
' The example displays the following output:
'       You do not have permission to access all folders in this path.
'       See your network administrator or try another path.
'
'       ArgumentException: The path is not of a legal form.

Task.Exception プロパティによる例外の確認

タスクが TaskStatus.Faulted 状態で完了した場合、その Exception プロパティを調べることで違反の原因となった例外を見つけることができます。 Exception プロパティを確認する場合は、次の例に示すように、継続元タスクが違反した場合にのみ実行される継続を使用することをお勧めします。


public static partial class Program
{
    public static void ExceptionPropagationTwo()
    {
        _ = Task.Run(
            () => throw new CustomException("task1 faulted."))
            .ContinueWith(_ =>
            {
                if (_.Exception?.InnerException is { } inner)
                {
                    Console.WriteLine("{0}: {1}",
                        inner.GetType().Name,
                        inner.Message);
                }
            }, 
            TaskContinuationOptions.OnlyOnFaulted);
        
        Thread.Sleep(500);
    }
}
// The example displays output like the following:
//        CustomException: task1 faulted.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Factory.StartNew(Sub()
                                              Throw New CustomException("task1 faulted.")
                                          End Sub).
                    ContinueWith(Sub(t)
                                     Console.WriteLine("{0}: {1}",
                                                     t.Exception.InnerException.GetType().Name,
                                                     t.Exception.InnerException.Message)
                                 End Sub, TaskContinuationOptions.OnlyOnFaulted)

        Thread.Sleep(500)
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays output like the following:
'       CustomException: task1 faulted.

重要なアプリケーションでは、継続のデリゲートで例外に関する詳細情報を記録して、新しいタスクを作成して例外から回復することが考えられます。 タスクで障害が発生した場合は、次の式によって例外をスローします。

  • await task
  • task.Wait()
  • task.Result
  • task.GetAwaiter().GetResult()

スローされた例外を処理したり観察したりするには、try-catch ステートメントを使用します。 Task.Exception プロパティにアクセスして例外を観察する方法もあります。

重要

次の式を使用する場合、AggregateException を明示的にキャッチすることはできません。

  • await task
  • task.GetAwaiter().GetResult()

UnobservedTaskException イベント

信頼関係のないプラグインをホストするときなど、シナリオによっては、問題のない例外がよく発生する場合や、難しすぎてすべてを手動で観察できなくなる場合があります。 このような場合、 TaskScheduler.UnobservedTaskException イベントを処理できます。 ハンドラーに渡される System.Threading.Tasks.UnobservedTaskExceptionEventArgs インスタンスを使用して、観察されない例外が連結しているスレッドに反映されないようにすることができます。

関連項目