Gestion des exceptions (bibliothèque parallèle de tâches)

Les exceptions non gérées levées par le code utilisateur s’exécutant à l’intérieur d’une tâche sont propagées vers le thread appelant, sauf dans certains scénarios décrits plus loin dans cette rubrique. Les exceptions sont propagées quand vous utilisez l’une des méthodes statiques ou d’instance Task.Wait et que vous les gérez en incluant l’appel dans une instruction try/catch. Si une tâche est le parent de tâches enfants attachées ou si vous attendez plusieurs tâches, plusieurs exceptions peuvent être levées.

Pour propager toutes les exceptions vers le thread appelant, l’infrastructure de la tâche les encapsule dans une instance AggregateException . L’exception AggregateException possède une propriété InnerExceptions qu’il est possible d’énumérer pour examiner toutes les exceptions d’origine levées et gérer (ou non) individuellement chacune d’elles. Vous pouvez également gérer les exceptions d’origine à l’aide de la méthode AggregateException.Handle.

Même si une seule exception est levée, elle est encapsulée dans une exception AggregateException , comme le montre l’exemple suivant.


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!

Vous pouvez éviter une exception non gérée en interceptant l’exception AggregateException et en n’observant aucune des exceptions internes. Toutefois, cela n’est pas recommandé car cela revient à intercepter le type Exception de base dans des scénarios non parallèles. Intercepter une exception sans prendre de mesures spécifiques de récupération peut laisser votre programme dans un état indéterminé.

Si vous ne voulez pas appeler la méthode Task.Wait afin d’attendre la fin d’une tâche, vous pouvez également récupérer l’exception AggregateException à partir de la propriété Exception de la tâche, comme le montre l’exemple suivant. Pour plus d’informations, consultez la section Observation d’exceptions à l’aide de la propriété Task.Exception dans cette rubrique.


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!

Attention

L’exemple de code précédent inclut une boucle while qui interroge la propriété de la tâche Task.IsCompleted pour déterminer quand la tâche est terminée. Cette opération ne doit jamais être effectuée dans le code de production, car elle est très inefficace.

Si vous n’attendez pas une tâche qui propage une exception ou accédez à sa propriété Exception , l’exception est transmise d’après la stratégie de l’exception .NET lorsque la tâche est récupérée par le garbage collector.

Lorsque les exceptions sont autorisées à se propager vers le thread joint, il est possible qu’une tâche continue à traiter des éléments après que l’exception a été levée.

Notes

Quand l’option Uniquement mon code est activée, Visual Studio, dans certains cas, peut s’arrêter sur la ligne qui lève l’exception et afficher un message d’erreur indiquant que l’exception n’est pas gérée par le code utilisateur. Cette erreur est sans gravité. Vous pouvez appuyer sur F5 pour continuer et voir le comportement de gestion des exceptions qui est illustré dans ces exemples. Pour empêcher Visual Studio de s’arrêter sur la première erreur, il suffit de désactiver la case à cocher Autoriser uniquement mon code sous Outils, Options, Débogage, Général.

Tâches enfants attachées et exceptions AggregateException imbriquées

Si une tâche a une tâche enfant attachée qui lève une exception, cette exception est encapsulée dans une exception AggregateException avant d’être propagée vers la tâche parent, qui encapsule cette exception dans sa propre exception AggregateException avant de la propager vers le thread appelant. Dans de tels cas, la propriété InnerExceptions de l’exception AggregateException interceptée au niveau de la méthode Task.Wait, WaitAny ou WaitAllcontient une ou plusieurs instances AggregateException, et non les exceptions d’origine ayant provoqué l’erreur. Pour éviter d’avoir à effectuer une itération sur les exceptions AggregateException imbriquées, vous pouvez utiliser la méthode Flatten pour supprimer toutes les exceptions AggregateException imbriquées, afin que la propriété AggregateException.InnerExceptions contienne les exceptions d’origine. Dans l’exemple suivant, les instances AggregateException imbriquées sont aplaties et gérées en une seule boucle.


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.

Vous pouvez également utiliser la méthode AggregateException.Flatten pour lever à nouveau les exceptions internes de plusieurs instances AggregateException levées par plusieurs tâches dans une seule instance AggregateException, comme le montre l’exemple suivant.

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.

Exceptions des tâches enfants détachées

Par défaut, les tâches enfants sont créées détachées. Les exceptions levées depuis des tâches détachées doivent être gérées ou levées à nouveau dans la tâche parent immédiate. Elles ne sont pas propagées vers le thread appelant de la même façon que les tâches enfants attachées. Le parent le plus haut peut lever à nouveau manuellement une exception à partir d’un enfant détaché pour l’encapsuler dans une exception AggregateException et la propager vers le thread appelant.


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.

Même si vous utilisez une continuation pour observer une exception dans une tâche enfant, l’exception doit toujours être observée par la tâche parent.

Exceptions indiquant une annulation coopérative

Lorsque le code utilisateur d’une tâche répond à une demande d’annulation, la procédure correcte consiste à lever une exception OperationCanceledException qui passe le jeton d’annulation sur lequel la demande a été communiquée. Avant d’essayer de propager l’exception, l’instance de tâche compare le jeton de l’exception à celui qui lui a été passé lors de sa création. S’ils sont identiques, la tâche propage une exception TaskCanceledException encapsulée dans l’exception AggregateException, et cette dernière peut être affichée au moment d’examiner les exceptions internes. Toutefois, si le thread appelant n’est pas en attente sur la tâche, cette exception spécifique n’est pas propagée. Pour plus d’informations, voir Annulation de tâches.

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)

Utilisation de la méthode Handle pour filtrer les exceptions internes

Vous pouvez utiliser la méthode AggregateException.Handle pour éliminer les exceptions que vous pouvez traiter comme « gérées » sans utiliser d’autre logique. Dans le délégué utilisateur fourni à la méthode AggregateException.Handle(Func<Exception,Boolean>) , vous pouvez examiner le type d’exception, sa propriété Message ou toute autre information le concernant qui vous permettra de déterminer si l’exception est sans gravité. Toutes les exceptions pour lesquelles le délégué retourne la valeur false sont levées à nouveau dans une nouvelle instance AggregateException dès le retour de la méthode AggregateException.Handle.

L’exemple suivant est fonctionnellement équivalent au premier exemple de cette rubrique, qui examine chaque exception de la collection AggregateException.InnerExceptions. À la place, ce gestionnaire d’exceptions appelle l’objet de la méthode AggregateException.Handle pour chaque exception et lève à nouveau uniquement les exceptions qui ne sont pas des instances 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!

L’exemple suivant est plus complet et utilise la méthode AggregateException.Handle pour fournir une gestion spéciale pour une exception UnauthorizedAccessException lors de l’énumération des fichiers.

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.

Observation d’exceptions à l’aide de la propriété Task.Exception

Si une tâche se termine avec l’état TaskStatus.Faulted, il est possible d’examiner sa propriété Exception pour découvrir l’exception spécifique qui a provoqué l’erreur. Un bon moyen d’observer la propriété Exception consiste à utiliser une continuation qui s’exécute uniquement en cas d’erreur de la tâche antérieure, comme indiqué dans l’exemple suivant.


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.

Dans une application concrète, le délégué de continuation peut consigner des informations détaillées sur l’exception et générer de nouvelles tâches pour se remettre de l’exception. En cas d’erreur d’une tâche, les expressions suivantes lèvent l’exception :

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

Utilisez une instruction try-catch pour gérer et observer les exceptions levées. Vous pouvez également observer l’exception en accédant à la propriété Task.Exception.

Important

AggregateException ne peut pas être intercepté explicitement lors de l’utilisation des expressions suivantes :

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

Événement UnobservedTaskException

Dans certains scénarios, tels que l’hébergement de plug-ins non approuvés, des exceptions sans gravité sont courantes. Il peut s’avérer trop difficile de toutes les observer manuellement. Dans de tels cas, vous pouvez gérer l’événement TaskScheduler.UnobservedTaskException. Il est possible d’utiliser l’instance System.Threading.Tasks.UnobservedTaskExceptionEventArgs passée à votre gestionnaire pour empêcher la propagation de l’exception non prise en charge vers le thread joint.

Voir aussi