Praktik terbaik untuk menangkap dan melempar kembali .NET pengecualian

284

Apa praktik terbaik yang harus dipertimbangkan ketika menangkap pengecualian dan melemparkannya kembali? Saya ingin memastikan bahwa jejak Exceptionobjek InnerExceptiondan tumpukan dipertahankan. Apakah ada perbedaan antara blok kode berikut dalam cara mereka menangani ini?

try
{
    //some code
}
catch (Exception ex)
{
    throw ex;
}

Vs:

try
{
    //some code
}
catch
{
    throw;
}
Seibar
sumber

Jawaban:

262

Cara untuk melestarikan jejak tumpukan adalah melalui penggunaan throw;Ini juga berlaku

try {
  // something that bombs here
} catch (Exception ex)
{
    throw;
}

throw ex;pada dasarnya seperti melempar pengecualian dari titik itu, jadi jejak tumpukan hanya akan pergi ke tempat Anda mengeluarkan throw ex;pernyataan.

Mike juga benar, dengan asumsi pengecualian memungkinkan Anda untuk melewati pengecualian (yang disarankan).

Karl Seguin memiliki tulisan yang bagus tentang penanganan pengecualian dalam fondasinya pemrograman e-book juga, yang merupakan bacaan yang bagus.

Sunting: Tautan yang berfungsi ke Yayasan Pemrograman pdf. Cukup cari teks untuk "pengecualian".

Darren Kopp
sumber
10
Saya tidak begitu yakin apakah penulisan itu bagus, itu menyarankan mencoba {// ...} catch (Exception ex) {throw Exception baru (ex.Message + "other stuff"); } bagus. Masalahnya adalah bahwa Anda benar-benar tidak dapat menangani pengecualian itu lebih jauh di stack, kecuali jika Anda menangkap semua pengecualian, tidak-tidak besar (Anda yakin Anda ingin menangani itu OutOfMemoryException?)
ljs
2
@ ljs Sudahkah artikel berubah sejak komentar Anda karena saya tidak melihat bagian mana pun yang dia rekomendasikan. Justru sebaliknya, dia mengatakan tidak melakukannya dan bertanya apakah Anda ingin menangani OutOfMemoryException juga !?
RyanfaeScotland
6
Terkadang melempar; tidak cukup untuk menyimpan jejak stack. Berikut ini adalah contoh https://dotnetfiddle.net/CkMFoX
Artavazd Balayan
4
Atau ExceptionDispatchInfo.Capture(ex).Throw(); throw;di .NET +4.5 stackoverflow.com/questions/57383/…
Alfred Wallace
Solusi @AlfredWallace bekerja dengan sempurna untuk saya. coba {...} catch {throw} tidak mempertahankan jejak tumpukan. Terima kasih.
atownson
100

Jika Anda melempar pengecualian baru dengan pengecualian awal, Anda juga akan mempertahankan jejak tumpukan awal ..

try{
} 
catch(Exception ex){
     throw new MoreDescriptiveException("here is what was happening", ex);
}
Mike
sumber
Apa pun yang saya coba lemparkan Pengecualian baru ("pesan", ex) selalu melempar ex dan mengabaikan pesan khusus. membuang Pengecualian baru ("message", ex.InnerException) berfungsi.
Tod
Jika tidak ada pengecualian khusus yang diperlukan, seseorang dapat menggunakan AggregateException (.NET 4+) msdn.microsoft.com/en-us/library/…
Nikos Tsokos
AggregateExceptionseharusnya hanya digunakan untuk pengecualian atas operasi agregat. Misalnya, itu dilemparkan oleh ParallelEnumerabledan Taskkelas-kelas CLR. Penggunaan mungkin harus mengikuti contoh ini.
Aluan Haddad
29

Sebenarnya, ada beberapa situasi dimana throwstatment tidak akan menyimpan informasi StackTrace. Misalnya, dalam kode di bawah ini:

try
{
  int i = 0;
  int j = 12 / i; // Line 47
  int k = j + 1;
}
catch
{
  // do something
  // ...
  throw; // Line 54
}

StackTrace akan menunjukkan bahwa baris 54 menaikkan pengecualian, meskipun itu dinaikkan pada baris 47.

Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
   at Program.WithThrowIncomplete() in Program.cs:line 54
   at Program.Main(String[] args) in Program.cs:line 106

Dalam situasi seperti yang dijelaskan di atas, ada dua opsi untuk preseve StackTrace asli:

Memanggil Exception.InternalPreserveStackTrace

Karena ini adalah metode pribadi, itu harus dipanggil dengan menggunakan refleksi:

private static void PreserveStackTrace(Exception exception)
{
  MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
    BindingFlags.Instance | BindingFlags.NonPublic);
  preserveStackTrace.Invoke(exception, null);
}

Saya memiliki kelemahan mengandalkan metode pribadi untuk menjaga informasi StackTrace. Itu bisa diubah di versi .NET Framework. Contoh kode di atas dan solusi yang diusulkan di bawah ini diambil dari weblog Fabrice MARGUERIE .

Memanggil Exception.SetObjectData

Teknik di bawah ini disarankan oleh Anton Tykhyy sebagai jawaban untuk In C #, bagaimana saya bisa memikirkan kembali InnerException tanpa kehilangan pertanyaan susunan jejak .

static void PreserveStackTrace (Exception e) 
{ 
  var ctx = new StreamingContext  (StreamingContextStates.CrossAppDomain) ; 
  var mgr = new ObjectManager     (null, ctx) ; 
  var si  = new SerializationInfo (e.GetType (), new FormatterConverter ()) ; 

  e.GetObjectData    (si, ctx)  ; 
  mgr.RegisterObject (e, 1, si) ; // prepare for SetObjectData 
  mgr.DoFixups       ()         ; // ObjectManager calls SetObjectData 

  // voila, e is unmodified save for _remoteStackTraceString 
} 

Meskipun, memiliki keuntungan mengandalkan metode publik hanya itu juga tergantung pada konstruktor pengecualian berikut (yang beberapa pengecualian yang dikembangkan oleh pihak ke-3 tidak diterapkan):

protected Exception(
    SerializationInfo info,
    StreamingContext context
)

Dalam situasi saya, saya harus memilih pendekatan pertama, karena pengecualian yang diajukan oleh perpustakaan pihak ketiga yang saya gunakan tidak menerapkan konstruktor ini.

CARLOS LOTH
sumber
1
Anda dapat menangkap pengecualian dan mempublikasikan pengecualian ini di mana pun Anda inginkan. Kemudian lempar yang baru menjelaskan apa yang terjadi pada pengguna. Dengan cara ini Anda bisa melihat apa yang terjadi pada saat pengecualian saat ditangkap, pengguna dapat mengabaikan apa pengecualian sebenarnya.
Çöđěxěŕ
2
Dengan .NET 4.5 ada opsi ketiga dan - menurut saya - bersih: gunakan ExceptionDispatchInfo. Lihat Tragedi menjawab pertanyaan terkait di sini: stackoverflow.com/a/17091351/567000 untuk info lebih lanjut.
Søren Boisen
20

Saat Anda throw ex, Anda pada dasarnya melempar pengecualian baru, dan akan kehilangan informasi tumpukan jejak yang asli. throwadalah metode yang disukai.

Titik Koma Lupa
sumber
13

Aturan praktisnya adalah untuk menghindari Catching and Throwing the basic Exceptionobject. Ini memaksa Anda untuk menjadi sedikit lebih pintar tentang pengecualian; dengan kata lain Anda harus memiliki tangkapan eksplisit untuk SqlExceptionkode penanganan Anda sehingga tidak melakukan sesuatu yang salah dengan a NullReferenceException.

Namun di dunia nyata, menangkap dan mencatat pengecualian basis juga merupakan praktik yang baik, tetapi jangan lupa untuk melakukan semuanya untuk mendapatkan apa InnerExceptionsyang mungkin ada.

swilliams
sumber
2
Saya pikir yang terbaik untuk berurusan dengan pengecualian tidak tertangani untuk keperluan logging dengan menggunakan AppDomain.CurrentDomain.UnhandledException dan Application.ThreadException pengecualian. Menggunakan percobaan besar {...} catch (Exception ex) {...} di mana-mana berarti banyak duplikasi. Tergantung apakah Anda ingin mencatat pengecualian yang ditangani, dalam hal ini (setidaknya minimal) duplikasi mungkin tidak terhindarkan.
ljs
Ditambah menggunakan peristiwa-peristiwa berarti Anda melakukan log semua eksepsi tidak tertangani, sedangkan jika Anda menggunakan ol besar' try {...} catch (Exception ex) {...} blok Anda mungkin kehilangan beberapa.
ljs
10

Anda harus selalu menggunakan "lemparan;" untuk memikirkan kembali pengecualian di .NET,

Lihat ini, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

Pada dasarnya MSIL (CIL) memiliki dua instruksi - "melempar" dan "rethrow":

  • "Throw ex;" akan dikompilasi ke dalam "lemparan" MSIL
  • "Lemparan;" - ke dalam MSIL "rethrow"!

Pada dasarnya saya dapat melihat alasan mengapa "throw ex" menimpa jejak stack.

Vinod T. Patil
sumber
Tautan - yah, sebenarnya sumber yang mengutip tautan - penuh dengan informasi yang baik, dan juga mencatat kemungkinan penyebab mengapa banyak orang berpikir throw ex;akan memikirkan kembali - di Jawa, ya! Tetapi Anda harus menyertakan informasi itu di sini untuk mendapatkan jawaban Grade A. (Meskipun saya masih mengejar ExceptionDispatchInfo.Capturejawaban dari jeuoekdcwzfwccu .)
ruffin
10

Tidak ada yang menjelaskan perbedaan antara ExceptionDispatchInfo.Capture( ex ).Throw()dan dataran throw, jadi ini dia. Namun, beberapa orang telah memperhatikan masalah tersebut throw.

Cara lengkap untuk mengubah kembali pengecualian yang tertangkap adalah menggunakan ExceptionDispatchInfo.Capture( ex ).Throw()(hanya tersedia dari .Net 4.5).

Di bawah ini ada kasus yang diperlukan untuk menguji ini:

1.

void CallingMethod()
{
    //try
    {
        throw new Exception( "TEST" );
    }
    //catch
    {
    //    throw;
    }
}

2.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        ExceptionDispatchInfo.Capture( ex ).Throw();
        throw; // So the compiler doesn't complain about methods which don't either return or throw.
    }
}

3.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch
    {
        throw;
    }
}

4.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        throw new Exception( "RETHROW", ex );
    }
}

Kasus 1 dan kasus 2 akan memberi Anda jejak tumpukan di mana nomor baris kode sumber untuk CallingMethod metode ini adalah nomor baris dari throw new Exception( "TEST" )baris tersebut.

Namun, kasus 3 akan memberi Anda jejak tumpukan di mana nomor baris kode sumber untuk CallingMethodmetode ini adalah nomor baristhrow panggilan. Ini berarti bahwa jika throw new Exception( "TEST" )garis dikelilingi oleh operasi lain, Anda tidak tahu di mana nomor garis pengecualian itu sebenarnya dilemparkan.

Kasus 4 mirip dengan kasus 2 karena nomor baris pengecualian asli dipertahankan, tetapi bukan rethrow nyata karena mengubah jenis pengecualian asli.

jeuoekdcwzfwccu
sumber
Tambahkan uraian sederhana untuk tidak pernah digunakan throw ex;dan ini adalah jawaban terbaik dari semuanya.
NH.
8

Beberapa orang benar-benar melewatkan poin yang sangat penting - 'melempar' dan 'melempar mantan' mungkin melakukan hal yang sama tetapi mereka tidak memberi Anda sepotong penting informasi yang merupakan garis di mana pengecualian terjadi.

Pertimbangkan kode berikut:

static void Main(string[] args)
{
    try
    {
        TestMe();
    }
    catch (Exception ex)
    {
        string ss = ex.ToString();
    }
}

static void TestMe()
{
    try
    {
        //here's some code that will generate an exception - line #17
    }
    catch (Exception ex)
    {
        //throw new ApplicationException(ex.ToString());
        throw ex; // line# 22
    }
}

Ketika Anda melakukan 'melempar' atau 'melempar mantan' Anda mendapatkan jejak tumpukan tetapi baris # akan menjadi # 22 sehingga Anda tidak dapat mengetahui baris mana yang melempar pengecualian secara tepat (kecuali Anda hanya memiliki 1 atau beberapa baris kode di blok coba). Untuk mendapatkan garis yang diharapkan # 17 di pengecualian Anda, Anda harus membuang pengecualian baru dengan jejak tumpukan pengecualian asli.

notlkk
sumber
3

Anda juga dapat menggunakan:

try
{
// Dangerous code
}
finally
{
// clean up, or do nothing
}

Dan setiap pengecualian yang dilemparkan akan menggelembung ke tingkat berikutnya yang menanganinya.

Erick B
sumber
3

Saya pasti akan menggunakan:

try
{
    //some code
}
catch
{
    //you should totally do something here, but feel free to rethrow
    //if you need to send the exception up the stack.
    throw;
}

Itu akan menghemat tumpukan Anda.

1kevgriff
sumber
1
Agar adil untuk melewati saya di tahun 2008, OP bertanya bagaimana cara melestarikan tumpukan - dan 2008 saya memberikan jawaban yang benar. Apa yang hilang dari jawaban saya adalah bagian dari melakukan sesuatu yang sebenarnya.
1kevgriff
@ JohnSaunders Itu benar jika dan hanya jika Anda tidak melakukan apa pun sebelumnya throw; misalnya Anda bisa membersihkan pakai (di mana Anda HANYA menyebutnya kesalahan) dan kemudian melemparkan pengecualian.
Meirion Hughes
@meirion ketika saya menulis komentar tidak ada sebelum lemparan. Ketika itu ditambahkan, saya memilih, tetapi tidak menghapus komentar.
John Saunders
0

FYI Saya baru saja menguji ini dan jejak stack dilaporkan oleh 'throw;' bukan jejak stack yang sepenuhnya benar. Contoh:

    private void foo()
    {
        try
        {
            bar(3);
            bar(2);
            bar(1);
            bar(0);
        }
        catch(DivideByZeroException)
        {
            //log message and rethrow...
            throw;
        }
    }

    private void bar(int b)
    {
        int a = 1;
        int c = a/b;  // Generate divide by zero exception.
    }

Jejak tumpukan menunjuk ke asal pengecualian dengan benar (nomor baris yang dilaporkan) tetapi nomor baris yang dilaporkan untuk foo () adalah garis lemparan; pernyataan, maka Anda tidak bisa mengatakan mana dari panggilan ke bilah () yang menyebabkan pengecualian.

redcalx
sumber
Itulah sebabnya mengapa lebih baik untuk tidak mencoba menangkap pengecualian kecuali jika Anda berencana untuk melakukan sesuatu dengan mereka
Nate Zaugg