Penanganan pengecualian tugas

Gunakan await sebagai default Anda. await memberikan alur pengecualian yang alami, menjaga kode Anda tetap mudah dibaca, dan menghindari deadlock sinkronisasi-di-atas-asinkron.

Terkadang Anda masih perlu memblokir Task, misalnya, di titik masuk sinkron warisan. Dalam kasus tersebut, Anda perlu memahami bagaimana setiap API menampilkan pengecualian.

Membandingkan propagasi pengecualian untuk memblokir API

Ketika Anda harus memblokir tugas, gunakan GetAwaiter(). GetResult() untuk mempertahankan jenis pengecualian asli:

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 dan Wait membungkus pengecualian dalam AggregateException, yang mempersulit penanganan pengecualian. Kode berikut menggunakan API ini dan menerima jenis pengecualian yang salah:

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

Untuk tugas yang mengalami kesalahan dengan beberapa pengecualian, GetAwaiter().GetResult() tetap melemparkan satu pengecualian, tetapi Task.Exception menyimpan AggregateException yang berisi semua pengecualian dalamnya.

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()

Gunakan panduan ini saat Anda memilih antara dua API:

  • Utamakan await jika memungkinkan. Ini menghindari risiko pemblokiran dan kebuntuan.
  • Jika Anda harus memblokir dan Anda menginginkan jenis pengecualian asli, gunakan GetAwaiter().GetResult(). Di aplikasi WinForms, perhatikan bagian Jebakan umum dan kebuntuan artikel tentang penanganan aktivitas.
  • Jika kode Anda yang ada mengharapkan AggregateException, gunakan Result atau Wait() dan periksa InnerExceptions.

Aturan ini hanya memengaruhi bentuk pengecualian. Kedua API masih memblokir utas saat ini, sehingga keduanya dapat kebuntuan pada lingkungan utas SynchronizationContext tunggal. Untuk memahami cara menyelesaikan tugas dengan benar di semua jalur kode, lihat Menyelesaikan tugas Anda.

Pengecualian tugas yang tidak teramati dalam .NET modern

Runtime memunculkan TaskScheduler.UnobservedTaskException ketika Task yang mengalami kesalahan dinyatakan sudah selesai sebelum kode mengamati pengecualiannya.

Dalam .NET modern, pengecualian tidak tertangkap tidak lagi merusak proses secara bawaan. Runtime melaporkannya melalui event, lalu melanjutkan proses eksekusi.

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

Gunakan peristiwa untuk diagnostik dan telemetri. Jangan gunakan peristiwa sebagai pengganti penanganan pengecualian normal dalam alur asinkron.

Baca juga