Gestione delle eccezioni nei processi delle attività

Usare await come impostazione predefinita. await offre un flusso di eccezioni naturale, mantiene il codice leggibile ed evita deadlock sincroni.

A volte è necessario bloccare su un Taskoggetto, ad esempio, nei punti di ingresso sincroni legacy. In questi casi, è necessario comprendere in che modo ogni API presenta le eccezioni.

Confrontare la propagazione delle eccezioni per le API di blocco

Quando è necessario bloccare un'attività, usa GetAwaiter().GetResult() per mantenere il tipo di eccezione originale.

public static class SingleExceptionExample
{
    public static Task<int> FaultAsync()
    {
        return Task.FromException<int>(new InvalidOperationException("Single failure"));
    }

    public static void ShowBlockingDifferences()
    {
        try
        {
            _ = FaultAsync().GetAwaiter().GetResult();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"GetAwaiter().GetResult() threw {ex.GetType().Name}");
        }
    }
}
Public Module SingleExceptionExample
    Public Function FaultAsync() As Task(Of Integer)
        Return Task.FromException(Of Integer)(New InvalidOperationException("Single failure"))
    End Function

    Public Sub ShowBlockingDifferences()
        Try
            Dim ignored = FaultAsync().GetAwaiter().GetResult()
        Catch ex As Exception
            Console.WriteLine($"GetAwaiter().GetResult() threw {ex.GetType().Name}")
        End Try
    End Sub
End Module

Task<TResult>.Result e Wait incapsulano le eccezioni in AggregateException, il che complica la gestione delle eccezioni. Il codice seguente usa queste API e riceve il tipo di eccezione errato:

// ⚠️ DON'T copy this snippet. It demonstrates a problem where exceptions get wrapped unnecessarily.
public static class SingleExceptionBadExample
{
    public static Task<int> FaultAsync()
    {
        return Task.FromException<int>(new InvalidOperationException("Single failure"));
    }

    public static void ShowBlockingDifferences()
    {
        try
        {
            _ = FaultAsync().Result;
        }
        catch (AggregateException ex)
        {
            Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}");
        }
    }
}
' ⚠️ DON'T copy this snippet. It demonstrates a problem where exceptions get wrapped unnecessarily.
Public Module SingleExceptionBadExample
    Public Function FaultAsync() As Task(Of Integer)
        Return Task.FromException(Of Integer)(New InvalidOperationException("Single failure"))
    End Function

    Public Sub ShowBlockingDifferences()
        Try
            Dim ignored = FaultAsync().Result
        Catch ex As AggregateException
            Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}")
        End Try
    End Sub
End Module

Per le attività con errori con più eccezioni, GetAwaiter().GetResult() genera comunque un'eccezione, ma Task.Exception archivia un oggetto AggregateException che contiene tutte le eccezioni interne:

public static class MultiExceptionExample
{
    public static async Task FaultAfterDelayAsync(string name, int milliseconds)
    {
        await Task.Delay(milliseconds);
        throw new InvalidOperationException($"{name} failed");
    }

    public static void ShowMultipleExceptions()
    {
        Task combined = Task.WhenAll(
            FaultAfterDelayAsync("First", 10),
            FaultAfterDelayAsync("Second", 20));

        try
        {
            combined.GetAwaiter().GetResult();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"GetAwaiter().GetResult() surfaced: {ex.Message}");
        }

        if (combined.IsFaulted && combined.Exception is not null)
        {
            AggregateException allErrors = combined.Exception.Flatten();
            Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions.");
        }
        else
        {
            Console.WriteLine("Task.Exception is null because the task didn't fault.");
        }
    }
}
Public Module MultiExceptionExample
    Public Async Function FaultAfterDelayAsync(name As String, milliseconds As Integer) As Task
        Await Task.Delay(milliseconds)
        Throw New InvalidOperationException($"{name} failed")
    End Function

    Public Sub ShowMultipleExceptions()
        Dim combined As Task = Task.WhenAll(
            FaultAfterDelayAsync("First", 10),
            FaultAfterDelayAsync("Second", 20))

        Try
            combined.GetAwaiter().GetResult()
        Catch ex As Exception
            Console.WriteLine($"GetAwaiter().GetResult() surfaced: {ex.Message}")
        End Try

        If combined.IsFaulted AndAlso combined.Exception IsNot Nothing Then
            Dim allErrors As AggregateException = combined.Exception.Flatten()
            Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions.")
        Else
            Console.WriteLine("Task.Exception was not available because the task did not fault.")
        End If
    End Sub
End Module

Confronto tra Task.Result e GetAwaiter().GetResult()

Usare queste indicazioni quando si sceglie tra le due API:

  • Preferisci await quando puoi. Evita il rischio di blocco e deadlock.
  • Se è necessario bloccare e si vogliono tipi di eccezione originali, usare GetAwaiter().GetResult(). Nelle applicazioni WinForms prendere nota della sezione Common pitfalls and deadlocks (Errori comuni e deadlock ) dell'articolo sui gestori eventi.
  • Se il codice esistente prevede AggregateException, usare Result o Wait() ed esaminare InnerExceptions.

Queste regole influiscono solo sulla forma delle eccezioni. Entrambe le API bloccano ancora il thread corrente, in modo che entrambi possano eseguire il deadlock in ambienti a SynchronizationContext thread singolo. Per informazioni su come completare correttamente le attività in tutti i percorsi di codice, vedere Completare le attività.

Eccezioni di attività non rilevate nelle .NET moderne

Il runtime genera TaskScheduler.UnobservedTaskException quando un errore Task viene finalizzato prima che il codice osservi l'eccezione.

Nelle versioni moderne di .NET, le eccezioni non rilevate non bloccano più il processo per impostazione predefinita. Il runtime li segnala tramite l'evento e quindi continua l'esecuzione.

public static class UnobservedTaskExceptionExample
{
    public static void ShowEventBehavior()
    {
        bool eventRaised = false;

        TaskScheduler.UnobservedTaskException += (_, args) =>
        {
            eventRaised = true;
            Console.WriteLine($"UnobservedTaskException raised with {args.Exception.InnerExceptions.Count} exception(s).");
            args.SetObserved();
        };

        _ = Task.Run(() => throw new ApplicationException("Background failure"));

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        Console.WriteLine(eventRaised
            ? "Event was raised. The process continued."
            : "Event was not observed in this short run. The process still continued.");
    }
}
Public Module UnobservedTaskExceptionExample
    Public Sub ShowEventBehavior()
        Dim eventRaised As Boolean = False

        AddHandler TaskScheduler.UnobservedTaskException,
            Sub(sender, args)
                eventRaised = True
                Console.WriteLine($"UnobservedTaskException raised with {args.Exception.InnerExceptions.Count} exception(s).")
                args.SetObserved()
            End Sub

        Task.Run(Sub() Throw New ApplicationException("Background failure"))

        GC.Collect()
        GC.WaitForPendingFinalizers()
        GC.Collect()

        If eventRaised Then
            Console.WriteLine("Event was raised. The process continued.")
        Else
            Console.WriteLine("Event was not observed in this short run. The process still continued.")
        End If
    End Sub
End Module

Usare l'evento per la diagnostica e i dati di telemetria. Non usare l'evento come sostituzione per la normale gestione delle eccezioni nei flussi asincroni.

Vedere anche