Bagikan melalui


Praktik terbaik untuk pengecualian

Penanganan pengecualian yang tepat sangat penting untuk keandalan aplikasi. Anda dapat dengan sengaja menangani pengecualian yang diharapkan untuk mencegah aplikasi mengalami kegagalan. Namun, aplikasi yang mengalami crash lebih andal dan dapat didiagnosis daripada aplikasi dengan perilaku yang tidak terdefinisi.

Artikel ini menjelaskan praktik terbaik untuk menangani dan membuat pengecualian.

Menangani pengecualian

Praktik terbaik berikut ini menyangkut cara Anda menangani pengecualian:

Gunakan blok try/catch/finally untuk memulihkan dari error atau melepaskan sumber daya

Untuk kode yang berpotensi menghasilkan pengecualian, dan kapan aplikasi Anda dapat pulih dari pengecualian tersebut, gunakan blok try/catch di sekitar kode. Dalam blok catch, selalu atur pengecualian dari yang paling khusus ke yang paling umum. (Semua pengecualian berasal dari kelas Exception. Pengecualian yang lebih turunan tidak ditangani oleh klausul catch yang didahului oleh klausa catch untuk kelas pengecualian dasar.) Saat kode Anda tidak dapat pulih dari pengecualian, jangan menangkap pengecualian tersebut. Aktifkan metode lebih jauh ke tumpukan panggilan untuk memulihkan jika memungkinkan.

Bersihkan sumber daya yang dialokasikan melalui pernyataan using atau blok finally. Lebih suka pernyataan using untuk membersihkan sumber daya secara otomatis saat pengecualian dilemparkan. Gunakan blok finally untuk membersihkan sumber daya yang tidak menerapkan IDisposable. Kode dalam klausa finally hampir selalu dijalankan bahkan ketika pengecualian dilemparkan.

Menangani kondisi umum untuk menghindari pengecualian

Untuk kondisi yang kemungkinan akan terjadi tetapi mungkin memicu pengecualian, pertimbangkan untuk menanganinya dengan cara yang menghindari pengecualian. Misalnya, jika Anda mencoba menutup koneksi yang sudah ditutup, Anda mendapatkan InvalidOperationException. Anda dapat menghindarinya dengan menggunakan pernyataan if untuk memeriksa status koneksi sebelum mencoba menutupnya.

if (conn.State != ConnectionState.Closed)
{
    conn.Close();
}
If conn.State <> ConnectionState.Closed Then
    conn.Close()
End IF

Jika Anda tidak memeriksa status koneksi sebelum menutupnya, Anda dapat menangkap pengecualian InvalidOperationException.

try
{
    conn.Close();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.GetType().FullName);
    Console.WriteLine(ex.Message);
}
Try
    conn.Close()
Catch ex As InvalidOperationException
    Console.WriteLine(ex.GetType().FullName)
    Console.WriteLine(ex.Message)
End Try

Pendekatan untuk memilih tergantung pada seberapa sering Anda mengharapkan peristiwa terjadi.

  • Gunakan penanganan pengecualian jika peristiwa tidak sering terjadi, yaitu, jika peristiwa benar-benar luar biasa dan menunjukkan kesalahan, seperti akhir file yang tidak terduga. Saat Anda menggunakan penanganan pengecualian, lebih sedikit kode yang dijalankan dalam kondisi normal.

  • Periksa kondisi kesalahan dalam kode jika peristiwa terjadi secara rutin dan dapat dianggap sebagai bagian dari eksekusi normal. Saat Anda memeriksa kondisi kesalahan umum, lebih sedikit kode yang dijalankan karena Anda menghindari pengecualian.

    Nota

    Pemeriksaan di muka menghilangkan pengecualian sebagian besar waktu. Namun, mungkin ada kondisi balapan di mana kondisi yang dijaga berubah antara pemeriksaan dan operasi, dan dalam hal ini, Anda masih dapat dikenakan pengecualian.

Panggil metode Try* agar terhindar dari pengecualian

Jika beban performa pengecualian terlalu tinggi, beberapa metode pustaka .NET menyediakan bentuk alternatif untuk penanganan kesalahan. Misalnya, Int32.Parse melempar OverflowException jika nilai yang akan diurai terlalu besar untuk diwakili oleh Int32. Namun, Int32.TryParse tidak melemparkan pengecualian ini. Sebaliknya, ia mengembalikan Boolean dan memiliki parameter out yang berisi bilangan bulat valid yang diurai setelah berhasil. Dictionary<TKey,TValue>.TryGetValue memiliki perilaku serupa untuk mencoba mendapatkan nilai dari kamus.

Menangkap pembatalan dan pengecualian asinkron

Lebih baik menangkap OperationCanceledException daripada TaskCanceledException, yang berasal dari OperationCanceledException, ketika Anda memanggil metode asinkron. Banyak metode asinkron melempar pengecualian OperationCanceledException jika pembatalan diminta. Pengecualian ini memungkinkan eksekusi dihentikan secara efisien dan tumpukan panggilan diurai setelah permintaan pembatalan terdeteksi.

Metode asinkron menyimpan pengecualian yang dilemparkan selama eksekusi dalam tugas yang mereka kembalikan. Jika pengecualian disimpan ke dalam tugas yang dikembalikan, pengecualian tersebut akan dilemparkan saat tugas ditunggu. Pengecualian penggunaan, seperti ArgumentException, masih dilemparkan secara sinkron. Untuk informasi selengkapnya, lihat pengecualian asinkron .

Rancang kelas sedemikian rupa sehingga pengecualian dapat dihindari

Kelas dapat menyediakan metode atau properti yang memungkinkan Anda menghindari panggilan yang akan memicu pengecualian. Misalnya, kelas FileStream menyediakan metode yang membantu menentukan apakah akhir file telah tercapai. Anda dapat memanggil metode ini untuk menghindari pengecualian yang dilemparkan jika Anda membaca melewati akhir file. Contoh berikut menunjukkan cara membaca ke akhir file tanpa memicu pengecualian:

class FileRead
{
    public static void ReadAll(FileStream fileToRead)
    {
        ArgumentNullException.ThrowIfNull(fileToRead);

        int b;

        // Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin);

        // Read each byte to the end of the file.
        for (int i = 0; i < fileToRead.Length; i++)
        {
            b = fileToRead.ReadByte();
            Console.Write(b.ToString());
            // Or do something else with the byte.
        }
    }
}
Class FileRead
    Public Sub ReadAll(fileToRead As FileStream)
        ' This if statement is optional
        ' as it is very unlikely that
        ' the stream would ever be null.
        If fileToRead Is Nothing Then
            Throw New System.ArgumentNullException()
        End If

        Dim b As Integer

        ' Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin)

        ' Read each byte to the end of the file.
        For i As Integer = 0 To fileToRead.Length - 1
            b = fileToRead.ReadByte()
            Console.Write(b.ToString())
            ' Or do something else with the byte.
        Next i
    End Sub
End Class

Cara lain untuk menghindari pengecualian adalah mengembalikan null (atau default) untuk kasus kesalahan yang paling umum alih-alih melemparkan pengecualian. Kasus kesalahan umum dapat dianggap sebagai aliran kontrol normal. Dengan mengembalikan null (atau default) dalam kasus ini, Anda meminimalkan dampak performa ke aplikasi.

Untuk jenis nilai, pertimbangkan apakah akan menggunakan Nullable<T> atau default sebagai indikator kesalahan untuk aplikasi Anda. Dengan menggunakan Nullable<Guid>, default menjadi null alih-alih Guid.Empty. Terkadang, menambahkan Nullable<T> dapat membuatnya lebih jelas ketika nilai ada atau tidak ada. Di lain waktu, menambahkan Nullable<T> dapat membuat kasus tambahan untuk memeriksa yang tidak diperlukan dan hanya berfungsi untuk membuat sumber kesalahan potensial.

Memulihkan status ketika metode tidak selesai karena pengecualian

Penelepon harus dapat mengasumsikan bahwa tidak ada efek samping ketika pengecualian terjadi dari sebuah metode. Misalnya, jika Anda memiliki kode yang mentransfer uang dengan menarik dari satu rekening dan menyetorkan di rekening lain, dan terjadi pengecualian saat melakukan penyetoran, Anda tidak ingin penarikan tetap terjadi.

public void TransferFunds(Account from, Account to, decimal amount)
{
    from.Withdrawal(amount);
    // If the deposit fails, the withdrawal shouldn't remain in effect.
    to.Deposit(amount);
}
Public Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    from.Withdrawal(amount)
    ' If the deposit fails, the withdrawal shouldn't remain in effect.
    [to].Deposit(amount)
End Sub

Metode sebelumnya tidak secara langsung melemparkan pengecualian apa pun. Namun, Anda harus menulis metode sehingga penarikan dibalik jika operasi setoran gagal.

Salah satu cara untuk menangani situasi ini adalah dengan menangkap pengecualian yang dilemparkan oleh transaksi deposit dan membatalkan penarikan.

private static void TransferFunds(Account from, Account to, decimal amount)
{
    string withdrawalTrxID = from.Withdrawal(amount);
    try
    {
        to.Deposit(amount);
    }
    catch
    {
        from.RollbackTransaction(withdrawalTrxID);
        throw;
    }
}
Private Shared Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    Dim withdrawalTrxID As String = from.Withdrawal(amount)
    Try
        [to].Deposit(amount)
    Catch
        from.RollbackTransaction(withdrawalTrxID)
        Throw
    End Try
End Sub

Contoh ini menggambarkan penggunaan throw untuk melempar ulang pengecualian asli, sehingga memudahkan pemanggil untuk memahami penyebab sebenarnya masalah tanpa harus memeriksa properti InnerException. Alternatifnya adalah melemparkan pengecualian baru dan menyertakan pengecualian asli sebagai pengecualian dalam.

catch (Exception ex)
{
    from.RollbackTransaction(withdrawalTrxID);
    throw new TransferFundsException("Withdrawal failed.", innerException: ex)
    {
        From = from,
        To = to,
        Amount = amount
    };
}
Catch ex As Exception
    from.RollbackTransaction(withdrawalTrxID)
    Throw New TransferFundsException("Withdrawal failed.", innerException:=ex) With
    {
        .From = from,
        .[To] = [to],
        .Amount = amount
    }
End Try

Menangkap dan melempar ulang pengecualian dengan benar

Setelah pengecualian dilemparkan, bagian dari informasi yang dikandungnya adalah jejak tumpukan. Pelacakan tumpukan adalah daftar hierarki panggilan metode yang dimulai dengan metode yang melempar pengecualian dan berakhir dengan metode yang menangkap pengecualian. Jika Anda melempar ulang pengecualian dengan menentukan pengecualian dalam pernyataan kode throw, misalnya throw e, pelacakan tumpukan dimulai ulang pada metode saat ini dan daftar panggilan metode antara metode asli yang melemparkan pengecualian dan metode saat ini hilang. Untuk menyimpan informasi pelacakan tumpukan asli dengan pengecualian, ada dua opsi yang tergantung di mana Anda melempar ulang pengecualian dari:

  • Jika Anda melempar ulang pengecualian dari dalam handler (blokcatch) yang menangkap instans pengecualian, gunakan pernyataan throw tanpa menyebutkan pengecualian. Aturan analisis kode CA2200 membantu Anda menemukan tempat dalam kode di mana Anda mungkin secara tidak sengaja kehilangan informasi pelacakan tumpukan.
  • Jika Anda melempar ulang pengecualian dari suatu tempat selain handler (blokcatch), gunakan ExceptionDispatchInfo.Capture(Exception) untuk menangkap pengecualian pada handler dan ExceptionDispatchInfo.Throw() saat Anda ingin melemparnya kembali. Anda dapat menggunakan properti ExceptionDispatchInfo.SourceException untuk memeriksa pengecualian yang ditangkap.

Contoh berikut menunjukkan bagaimana kelas ExceptionDispatchInfo dapat digunakan, dan seperti apa outputnya.

ExceptionDispatchInfo? edi = null;
try
{
    var txt = File.ReadAllText(@"C:\temp\file.txt");
}
catch (FileNotFoundException e)
{
    edi = ExceptionDispatchInfo.Capture(e);
}

// ...

Console.WriteLine("I was here.");

if (edi is not null)
    edi.Throw();

Jika file dalam kode contoh tidak ada, output berikut diproduksi:

I was here.
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\temp\file.txt'.
File name: 'C:\temp\file.txt'
   at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.File.ReadAllText(String path, Encoding encoding)
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 12
--- End of stack trace from previous location ---
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 24

Memunculkan pengecualian

Praktik terbaik berikut menyangkut bagaimana Anda melemparkan pengecualian:

Gunakan jenis pengecualian yang telah ditentukan sebelumnya

Perkenalkan kelas pengecualian baru hanya ketika kelas yang telah ditentukan sebelumnya tidak berlaku. Misalnya:

  • Jika kumpulan properti atau panggilan metode tidak sesuai mengingat status objek saat ini, berikan pengecualian InvalidOperationException.
  • Jika parameter yang tidak valid diteruskan, berikan pengecualian ArgumentException atau salah satu kelas yang telah ditentukan sebelumnya yang berasal dari ArgumentException.

Nota

Meskipun yang terbaik adalah menggunakan jenis pengecualian yang telah ditentukan sebelumnya jika memungkinkan, Anda tidak boleh menaikkan beberapa jenis pengecualian yang dicadangkan, seperti AccessViolationException, IndexOutOfRangeException, NullReferenceException dan StackOverflowException. Untuk informasi lebih lanjut, lihat CA2201: Jangan memunculkan jenis pengecualian yang sudah ditentukan.

Menggunakan metode penyusun pengecualian

Adalah umum bagi kelas untuk melemparkan pengecualian yang sama dari berbagai tempat dalam implementasinya. Untuk menghindari kode yang berlebihan, buat metode pembantu yang membuat pengecualian dan mengembalikannya. Misalnya:

class FileReader
{
    private readonly string _fileName;

    public FileReader(string path)
    {
        _fileName = path;
    }

    public byte[] Read(int bytes)
    {
        byte[] results = FileUtils.ReadFromFile(_fileName, bytes) ?? throw NewFileIOException();
        return results;
    }

    static FileReaderException NewFileIOException()
    {
        string description = "My NewFileIOException Description";

        return new FileReaderException(description);
    }
}
Class FileReader
    Private fileName As String


    Public Sub New(path As String)
        fileName = path
    End Sub

    Public Function Read(bytes As Integer) As Byte()
        Dim results() As Byte = FileUtils.ReadFromFile(fileName, bytes)
        If results Is Nothing
            Throw NewFileIOException()
        End If
        Return results
    End Function

    Function NewFileIOException() As FileReaderException
        Dim description As String = "My NewFileIOException Description"

        Return New FileReaderException(description)
    End Function
End Class

Beberapa jenis pengecualian .NET utama memiliki metode pembantu throw statis yang mengalokasikan dan melemparkan pengecualian. Anda harus memanggil metode ini alih-alih membangun dan melemparkan jenis pengecualian yang sesuai:

Petunjuk

Aturan analisis kode berikut dapat membantu Anda menemukan tempat dalam kode Anda di mana Anda dapat memanfaatkan pembantu throw statis ini: CA1510, CA1511, CA1512, dan CA1513.

Jika Anda menerapkan metode asinkron, panggil CancellationToken.ThrowIfCancellationRequested() alih-alih memeriksa apakah pembatalan diminta lalu membuat dan melemparkan OperationCanceledException. Untuk informasi selengkapnya, lihat CA2250.

Menyertakan pesan string yang dilokalkan

Pesan kesalahan yang dilihat pengguna berasal dari properti Exception.Message pengecualian yang dilemparkan, dan bukan dari nama kelas pengecualian. Biasanya, Anda menetapkan nilai ke properti Exception.Message dengan meneruskan string pesan ke argumen message konstruktor Pengecualian .

Untuk aplikasi yang dilokalkan, Anda harus menyediakan string pesan yang dilokalkan untuk setiap pengecualian yang dapat dilemparkan aplikasi Anda. Anda menggunakan file sumber daya untuk menyediakan pesan kesalahan yang dilokalkan. Untuk informasi tentang melokalisasi aplikasi dan mengambil string yang dilokalkan, lihat artikel berikut ini:

Gunakan tata bahasa yang tepat

Tulis kalimat yang jelas dan sertakan tanda baca akhir. Setiap kalimat dalam string yang ditetapkan ke properti Exception.Message harus berakhir dengan tanda titik. Misalnya, "Tabel log telah meluap." menggunakan tata bahasa serta tanda baca yang tepat.

Tempatkan pernyataan 'throw' dengan tepat

Tempatkan pernyataan throw di mana penelusuran tumpukan sangat membantu. Stack trace dimulai pada pernyataan di mana pengecualian dilemparkan dan berakhir pada pernyataan catch yang menangkap pengecualian.

Jangan memunculkan pengecualian dalam klausul akhirnya

Jangan ajukan pengecualian dalam klausul finally. Untuk informasi selengkapnya, lihat aturan analisis kode CA2219.

Jangan memunculkan pengecualian dari tempat yang tidak terduga

Beberapa metode, seperti metode Equals, GetHashCode, dan ToString, konstruktor statis, dan operator kesetaraan, tidak boleh melempar pengecualian. Untuk informasi selengkapnya, lihat aturan analisis kode CA1065.

Melempar pengecualian validasi argumen secara sinkron

Dalam metode pengembalian tugas, Anda harus memvalidasi argumen dan melemparkan pengecualian yang sesuai, seperti ArgumentException dan ArgumentNullException, sebelum memasukkan bagian asinkron dari metode . Pengecualian yang dilemparkan di bagian asinkron dari metode disimpan dalam tugas yang dikembalikan dan tidak muncul sampai, misalnya, tugas ditunggu. Untuk informasi selengkapnya, lihat Pengecualian dalam metode pengembalian tugas.

Jenis pengecualian kustom

Praktik terbaik berikut menyangkut jenis pengecualian kustom:

Akhiri nama kelas pengecualian dengan Exception

Ketika pengecualian kustom diperlukan, beri nama dengan tepat dan dapatkan dari kelas Exception. Misalnya:

public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
    Inherits Exception
End Class

Sertakan tiga konstruktor

Gunakan setidaknya tiga konstruktor umum saat membuat kelas pengecualian Anda sendiri: konstruktor tanpa parameter, konstruktor yang mengambil pesan string, dan konstruktor yang mengambil pesan string dan pengecualian dalam.

Misalnya, lihat Cara: Membuat pengecualian yang ditentukan pengguna.

Menyediakan lebih banyak properti sesuai kebutuhan

Berikan lebih banyak properti untuk pengecualian (selain string pesan kustom) hanya ketika ada skenario terprogram di mana informasi tambahan berguna. Misalnya, FileNotFoundException menyediakan properti FileName.

Lihat juga