Gestion d'exception avec les Tasks

Tags: Thread

Nous avons vu dans un précédent article comment utiliser Task pour effectuer des opérations dans un autre thread.

Malheureusement, comme tout le monde n’est pas parfait, il se peut que cette exécution se passe mal et qu’une exception se produise. Reprenons notre application WPF :

<StackPanel>
    <Button Content="Clic" Click="Button_Click" />
    <TextBlock x:Name="infos"/>
</StackPanel>

 

Avec un code qui génère une exception :

private void Button_Click(object sender, RoutedEventArgs e)
{
    infos.Text = "Exécution en cours....";

    Task<string>.Factory
        .StartNew(
            () =>
                {
                    Thread.Sleep(500);
                    throw new InvalidOperationException("Pas cool !!!");
                    return DateTime.Now.ToString();
                })
        .ContinueWith(
            task =>
                {
                    infos.Text = task.Result;
                },
            TaskScheduler.FromCurrentSynchronizationContext());
}

Autant le dire tout de suite : ce code, c’est MAL Clignement d'œil

Si vous distribuez ce code en mode release, vos clients risquent de se retrouver face à une jolie petite fenêtre :

TaskError

Pas cool. Et surtout, cette fenêtre n’apparaitra pas immédiatement au clic sur le bouton. Comme rien ne se passera immédiatement, vous pouvez avoir un utilisateur qui cliquera une dizaine de fois (ce que j’appelle CA, la “Cliquette Aigüe”) avant de voir cette fenêtre de la mort qui tue. Normalement, dans les secondes qui suivent, il vous appelle au téléphone en vous incendiant de divers mots que l’éthique m’interdit de reproduire ici (oui ils sont comme cela mes clients Tire la langue).

Y’a t’il une erreur ?

La Task de la méthode ContinueWith possède heureusement une propriété IsFaulted vous indiquant si il y a eu une ou des exceptions de déclenchées dans votre code. Donc on pourrait modifier notre précédent code ainsi :

private void Button_Click(object sender, RoutedEventArgs e)
{
    infos.Text = "Exécution en cours....";

    Task<string>.Factory
        .StartNew(
            () =>
                {
                    Thread.Sleep(500);
                    throw new InvalidOperationException("Pas cool !!!");
                    return DateTime.Now.ToString();
                })
        .ContinueWith(
            task =>
                {
                    if (task.IsFaulted)
                    {
                        infos.Text = string.Format("Il y a eu des erreurs");
                    }
                    else
                    {
                        infos.Text = task.Result;
                    }
                },
            TaskScheduler.FromCurrentSynchronizationContext());
}

 

Mais non, cela n’est toujours pas correct. Pourquoi ? Si vous cliquez intensivement sur le bouton, vous verrez qu’à terme, votre application se plantera telle une vache tentant de faire du saut à l’élastique.

Pourquoi la vache se plantera ? Parce qu’elle ne sait pas voler !

Pourquoi votre code plantera ? Parce que le thread ne sera pas libéré. Il existera toujours une référence vers un TaskExceptionHolder qui empêchera le garbage collector de libérer votre Task, donc de supprimer le thread. Le seul moyen de le libérer correctement, est simplement d’accéder à la propriété Exception de votre Task :

private void Button_Click(object sender, RoutedEventArgs e)
{
    infos.Text = "Exécution en cours....";

    Task<string>.Factory
        .StartNew(
            () =>
                {
                    Thread.Sleep(500);
                    throw new InvalidOperationException("Pas cool !!!");
                    return DateTime.Now.ToString();
                })
        .ContinueWith(
            task =>
                {
                    if (task.IsFaulted)
                    {
                        var err = task.Exception;
                        infos.Text = string.Format("Il y a eu des erreurs : {0}", err.Message);
                    }
                    else
                    {
                        infos.Text = task.Result;
                    }
                },
            TaskScheduler.FromCurrentSynchronizationContext());
}

 

Cette fois ci, on tient le bon bout. Sauf que le message qui sera affiché ne sera pas le message de votre exception (“Pas cool !!!”) mais : Une ou plusieurs erreurs se sont produites.

AggregateException

En réalité, la propriété Exception de votre Task est toujours une AggregateException qui possède une propriété de type collection contenant l’ensemble des exceptions qui ont pu se produire dans votre code (votre thread peut créer d’autres threads avec des erreurs, etc.). Donc, dans notre cas ou il n’y a qu’une exception qui peut se produire, on peut accéder directement à cette exception via la propriété InnerException de l’Exception :

if (task.IsFaulted)
{
    var err = task.Exception.InnerException;
    infos.Text = string.Format("Il y a eu une erreur : {0}", err);
}
else
{
    infos.Text = task.Result;
}

 

AggregateException typée

Reste que si l’on écrit du code proprement, il faut utiliser le typage des exceptions.

Tout dépend comment vous avez l’habitude de gérer les exceptions, mais pour ma part, si c’est une exception à laquelle je ne m’attends pas, je fais planter l’application (je vous assure que c’est le meilleur moyen d’avoir un code “solide”).

Donc il faut gérer les exceptions auxquelles on s’attend. Pour cela, AggregateException possède une méthode Handle qui attend un prédicat. Dans ce prédicat, vous pouvez dire si vous gérez ou non l’exception déclenchée. Dans notre exemple, cela donne ceci :

if (task.IsFaulted)
{
    task.Exception.Handle(ex =>
                   {
                       if (ex.GetType() == typeof (InvalidOperationException))
                       {
                           infos.Text = "On s'y attendait à celle là";
                           return true;
                       }
                       infos.Text = "Argggg!!!";
                       return false;
                   });
}
else
{
    infos.Text = task.Result;
}

 

Notre prédicat retourne True pour les InvalidOperationException et False pour les autres (ca plante).

NB : AggregateException possède une méthode Flatten qui retourne une AggregateException avec un seul niveau de profondeur des exceptions.

blog comments powered by Disqus