Aufgaben-Fehlerbehandlung

Verwenden Sie await als Standard. await bietet einen natürlichen Ausnahmefluss, trägt zur Lesbarkeit Ihres Codes bei und vermeidet Sync-über-Async-Deadlocks.

Manchmal müssen Sie immer noch auf einem Task, z. B. in älteren synchronen Einstiegspunkten, blockieren. In diesen Fällen müssen Sie verstehen, wie die einzelnen APIs Ausnahmen handhaben.

Vergleich der Ausbreitung von Ausnahmen bei blockierenden APIs

Wenn Sie eine Aufgabe blockieren müssen, verwenden Sie GetAwaiter(). GetResult() zum Beibehalten des ursprünglichen Ausnahmetyps:

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 und Wait umschließen Ausnahmen in AggregateException, wodurch die Ausnahmebehandlung erschwert wird. Der folgende Code verwendet diese APIs und empfängt den falschen Ausnahmetyp:

// ⚠️ 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

Bei Vorgängen, die mit mehreren Ausnahmen fehlschlagen, löst GetAwaiter().GetResult() weiterhin eine Ausnahme aus, jedoch speichert Task.Exception eine AggregateException, die alle inneren Ausnahmen enthält.

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

Task.Result Vs GetAwaiter().GetResult()

Verwenden Sie diese Anleitung, wenn Sie zwischen den beiden APIs wählen:

  • Bevorzugen Sie await, wenn Sie können. Dadurch werden Blockierung und Deadlock-Risiko vermieden.
  • Wenn Sie blockieren müssen und die ursprünglichen Ausnahmetypen beibehalten möchten, verwenden Sie GetAwaiter().GetResult(). Beachten Sie in WinForms-Anwendungen den Abschnitt "Häufige Stolpersteine" und "Deadlocks" des Artikels zu Ereignishandlern.
  • Wenn Ihr vorhandener Code AggregateException erwartet, verwenden Sie Result oder Wait() und prüfen Sie InnerExceptions.

Diese Regeln wirken sich nur auf die Ausnahmeform aus. Beide APIs blockieren den aktuellen Thread weiterhin, sodass beide in Singlethread-Umgebungen deadlocken SynchronizationContext können. Informationen zum ordnungsgemäßen Ausführen von Aufgaben in allen Codepfaden finden Sie unter "Ausführen Ihrer Aufgaben".

Ausnahmen für nicht überwachte Aufgaben in modernen .NET

Die Laufzeit löst TaskScheduler.UnobservedTaskException aus, wenn ein fehlerhaftes Task abgeschlossen wird, bevor der Code seine Ausnahme erfasst.

In aktuellen .NET-Versionen stürzt der Prozess standardmäßig nicht mehr aufgrund nicht beobachteter Ausnahmen ab. Die Laufzeit meldet sie über das Ereignis und setzt dann die Ausführung fort.

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

Verwenden Sie das Ereignis für Diagnose und Telemetrie. Verwenden Sie dieses Ereignis nicht als Ersatz für die normale Ausnahmebehandlung in asynchronen Abläufen.

Siehe auch