Mengapa gaya buruk `penyelamatan Exception => e` di Ruby?

895

Ruby QuickRef dari Ryan Davis berkata (tanpa penjelasan):

Jangan menyelamatkan Exception. PERNAH. atau aku akan menusukmu.

Kenapa tidak? Apa hal yang benar untuk dilakukan?

John
sumber
35
Maka Anda mungkin bisa menulis sendiri? :)
Sergio Tulentsev
65
Saya sangat tidak nyaman dengan seruan kekerasan di sini. Ini hanya pemrograman.
Darth Egregious
1
Lihatlah artikel ini di Ruby Exception dengan Hirarki Ruby Exception yang bagus .
Atul Khanduri
2
Karena Ryan Davis akan menusukmu. Jadi anak-anak. Jangan pernah menyelamatkan Pengecualian.
Mugen
7
@ DarthEgregious Saya tidak bisa memastikan apakah Anda bercanda atau tidak. Tapi saya pikir itu lucu. (Dan itu jelas bukan ancaman serius). Sekarang setiap kali saya berpikir tentang menangkap Exception, saya mempertimbangkan apakah layak ditusuk oleh beberapa pria acak di Internet.
Steve Sether

Jawaban:

1375

TL; DR : Gunakan StandardErrorsebagai ganti untuk menangkap pengecualian umum. Saat pengecualian asli dimunculkan kembali (mis. Saat menyelamatkan hanya untuk mencatat pengecualian), penyelamatan Exceptionmungkin tidak apa-apa.


Exceptionadalah akar dari hirarki pengecualian Ruby , jadi ketika Anda rescue ExceptionAnda menyelamatkan dari segala sesuatu , termasuk subclass seperti SyntaxError, LoadError, dan Interrupt.

Menyelamatkan Interruptmencegah pengguna CTRLCuntuk keluar dari program.

Menyelamatkan SignalExceptionmencegah program merespon dengan benar terhadap sinyal. Ini tidak akan dapat dikerjakan kecuali oleh kill -9.

Menyelamatkan SyntaxErrorberarti bahwa evalyang gagal akan melakukannya secara diam-diam.

Semua ini dapat ditampilkan dengan menjalankan program ini, dan mencoba CTRLCatau killmelakukannya:

loop do
  begin
    sleep 1
    eval "djsakru3924r9eiuorwju3498 += 5u84fior8u8t4ruyf8ihiure"
  rescue Exception
    puts "I refuse to fail or be stopped!"
  end
end

Menyelamatkan dari Exceptionbahkan bukan default. Perbuatan

begin
  # iceberg!
rescue
  # lifeboats
end

tidak menyelamatkan dari Exception, itu menyelamatkan dari StandardError. Anda biasanya harus menentukan sesuatu yang lebih spesifik daripada standar StandardError, tetapi menyelamatkan dari Exception memperluas ruang lingkup daripada mempersempitnya, dan dapat memiliki hasil bencana dan membuat perburuan bug sangat sulit.


Jika Anda memiliki situasi di mana Anda ingin menyelamatkan StandardErrordan Anda membutuhkan variabel dengan pengecualian, Anda dapat menggunakan formulir ini:

begin
  # iceberg!
rescue => e
  # lifeboats
end

yang setara dengan:

begin
  # iceberg!
rescue StandardError => e
  # lifeboats
end

Salah satu dari beberapa kasus umum yang harus diselamatkan Exceptionadalah untuk tujuan logging / pelaporan, dalam hal ini Anda harus segera mengajukan kembali pengecualian:

begin
  # iceberg?
rescue Exception => e
  # do some logging
  raise # not enough lifeboats ;)
end
Andrew Marshall
sumber
129
jadi seperti menangkap Throwabledi java
ratchet freak
53
Saran ini bagus untuk lingkungan Ruby yang bersih. Namun sayangnya sejumlah permata telah membuat pengecualian yang langsung turun dari Pengecualian. Lingkungan kami memiliki 30 di antaranya: misalnya OpenID :: Server :: EncodingError, OAuth :: InvalidRequest, HTMLTokenizerSample. Ini adalah pengecualian yang sangat ingin Anda tangkap di blok penyelamat standar. Sayangnya, tidak ada di Ruby yang mencegah atau bahkan mencegah permata mewarisi langsung dari Pengecualian - bahkan penamaannya pun tidak intuitif.
Jonathan Swartz
20
@ JonathanSwartz Kemudian selamatkan dari subclass spesifik itu, bukan Exception. Lebih spesifik hampir selalu lebih baik dan lebih jelas.
Andrew Marshall
22
@ JonathanSwartz - Saya akan bug pencipta permata untuk mengubah dari apa pengecualian mereka berasal. Secara pribadi, saya suka permata saya memiliki semua pengecualian turun dari MyGemException, sehingga Anda bisa menyelamatkannya jika Anda mau.
Nathan Long
12
Anda juga dapat ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]dan kemudianrescue *ADAPTER_ERRORS => e
j_mcnally
83

The nyata aturan adalah: Jangan membuang pengecualian. Objektivitas penulis kutipan Anda dipertanyakan, sebagaimana dibuktikan oleh fakta bahwa itu berakhir dengan

atau aku akan menusukmu

Tentu saja, perlu diketahui bahwa sinyal (secara default) melempar pengecualian, dan proses yang biasanya berjalan lama diakhiri melalui sinyal, jadi menangkap Pengecualian dan tidak berhenti pada pengecualian sinyal akan membuat program Anda sangat sulit untuk berhenti. Jadi jangan lakukan ini:

#! /usr/bin/ruby

while true do
  begin
    line = STDIN.gets
    # heavy processing
  rescue Exception => e
    puts "caught exception #{e}! ohnoes!"
  end
end

Tidak, sungguh, jangan lakukan itu. Bahkan jangan jalankan itu untuk melihat apakah itu berfungsi.

Namun, katakan Anda memiliki server berulir dan Anda ingin semua pengecualian tidak:

  1. diabaikan (default)
  2. hentikan server (yang terjadi jika Anda katakan thread.abort_on_exception = true).

Maka ini dapat diterima di utas penanganan koneksi Anda:

begin
  # do stuff
rescue Exception => e
  myLogger.error("uncaught #{e} exception while handling connection: #{e.message}")
    myLogger.error("Stack trace: #{backtrace.map {|l| "  #{l}\n"}.join}")
end

Di atas berfungsi untuk variasi penangan pengecualian standar Ruby, dengan keuntungan bahwa itu tidak juga membunuh program Anda. Rails melakukan ini dalam penangan permintaannya.

Pengecualian sinyal dimunculkan di utas utama. Utas latar belakang tidak akan mendapatkannya, jadi tidak ada gunanya mencoba menangkapnya di sana.

Ini sangat berguna dalam lingkungan produksi, di mana Anda tidak ingin program Anda berhenti begitu saja ketika ada masalah. Kemudian Anda dapat mengambil tumpukan dump di log Anda dan menambahkan ke kode Anda untuk menangani pengecualian spesifik lebih lanjut di rantai panggilan dan dengan cara yang lebih anggun.

Perhatikan juga bahwa ada idiom Ruby lain yang memiliki efek yang sama:

a = do_something rescue "something else"

Di baris ini, jika do_somethingmemunculkan pengecualian, itu ditangkap oleh Ruby, dibuang, dan aditugaskan "something else".

Secara umum, jangan lakukan itu, kecuali dalam kasus khusus di mana Anda tahu Anda tidak perlu khawatir. Satu contoh:

debugger rescue nil

The debuggerFungsi adalah cara yang agak bagus untuk mengatur breakpoint dalam kode Anda, tetapi jika berjalan di luar debugger, dan Rails, hal itu menimbulkan pengecualian. Sekarang secara teoritis Anda seharusnya tidak meninggalkan kode debug di sekitar program Anda (pff! Tidak ada yang melakukan itu!) Tetapi Anda mungkin ingin menyimpannya di sana untuk sementara waktu untuk beberapa alasan, tetapi tidak terus menjalankan debugger Anda.

catatan:

  1. Jika Anda menjalankan program orang lain yang menangkap pengecualian sinyal dan mengabaikannya, (katakan kode di atas), maka:

    • di Linux, di shell, ketik pgrep ruby, atau ps | grep ruby, cari PID program Anda yang menyinggung, dan kemudian jalankan kill -9 <PID>.
    • di Windows, gunakan Task Manager ( CTRL- SHIFT- ESC), buka tab "proses", cari proses Anda, klik kanan dan pilih "Akhiri proses".
  2. Jika Anda bekerja dengan program orang lain yang, untuk alasan apa pun, dibumbui dengan blok pengabaian ini, maka menempatkan ini di bagian atas garis utama adalah salah satu cara untuk keluar:

    %W/INT QUIT TERM/.each { |sig| trap sig,"SYSTEM_DEFAULT" }

    Hal ini menyebabkan program merespons sinyal terminasi normal dengan segera menghentikan, melewati penangan pengecualian, tanpa pembersihan . Sehingga bisa menyebabkan kehilangan data atau sejenisnya. Hati-hati!

  3. Jika Anda perlu melakukan ini:

    begin
      do_something
    rescue Exception => e
      critical_cleanup
      raise
    end

    Anda benar-benar dapat melakukan ini:

    begin
      do_something
    ensure
      critical_cleanup
    end

    Dalam kasus kedua, critical cleanupakan dipanggil setiap kali, apakah ada pengecualian atau tidak.

Michael Slade
sumber
21
Maaf, ini salah. Server seharusnya tidak pernah menyelamatkan Exception dan tidak melakukan apa pun kecuali mencatatnya. Itu akan membuatnya tidak dapat diraih kecuali oleh kill -9.
John
8
Contoh Anda dalam catatan 3 tidak sama, sebuah ensureakan berjalan terlepas dari apakah ada pengecualian yang dimunculkan atau tidak, sementara rescueitu hanya akan berjalan jika pengecualian muncul.
Andrew Marshall
1
Mereka tidak / persis / setara tetapi saya tidak tahu cara untuk secara ringkas mengekspresikan kesetaraan dengan cara yang tidak jelek.
Michael Slade
3
Cukup tambahkan panggilan critical_cleanup lain setelah blok begin / rescue dalam contoh pertama. Saya setuju bukan kode yang paling elegan, tapi jelas contoh kedua adalah cara yang elegan untuk melakukannya, jadi sedikit ketidakhadiran hanyalah bagian dari contoh.
gtd
3
"Bahkan jangan jalankan itu untuk melihat apakah itu berhasil." tampaknya saran yang buruk untuk pengkodean ... Sebaliknya, saya akan menyarankan Anda untuk menjalankannya, melihatnya gagal dan memahami sendiri bagaimana jika gagal, alih-alih memercayai orang lain secara membabi buta.
Bagaimanapun
69

TL; DR

Jangan rescue Exception => e(dan jangan naikkan kembali pengecualian) - atau Anda bisa mengusir jembatan.


Katakanlah Anda berada di dalam mobil (menjalankan Ruby). Anda baru-baru ini menginstal roda kemudi baru dengan sistem peningkatan over-the-air (yang menggunakan eval), tetapi Anda tidak tahu salah satu programmer mengacaukan sintaks.

Anda berada di jembatan, dan menyadari bahwa Anda akan sedikit ke arah pagar, jadi Anda belok kiri.

def turn_left
  self.turn left:
end

Ups! Itu mungkin Tidak Bagus ™, untungnya, Ruby memunculkan SyntaxError.

Mobil harus segera berhenti - bukan?

Nggak.

begin
  #...
  eval self.steering_wheel
  #...
rescue Exception => e
  self.beep
  self.log "Caught #{e}.", :warn
  self.log "Logged Error - Continuing Process.", :info
end

bip bip

Peringatan: Pengecualian Tertangkap Sintaks.

Info: Kesalahan Tercatat - Proses Berlanjut.

Anda melihat sesuatu yang salah, dan Anda membanting pada istirahat darurat ( ^C: Interrupt)

bip bip

Peringatan: Pengecualian Terperangkap Terperangkap.

Info: Kesalahan Tercatat - Proses Berlanjut.

Ya - itu tidak banyak membantu. Anda cukup dekat dengan rel, jadi Anda menempatkan mobil di parkir ( killing:) SignalException.

bip bip

Peringatan: Pengecualian SignalException Tertangkap.

Info: Kesalahan Tercatat - Proses Berlanjut.

Pada detik terakhir, Anda mengeluarkan kunci ( kill -9), dan mobil berhenti, Anda membanting ke depan ke roda kemudi (airbag tidak dapat mengembang karena Anda tidak menghentikan program dengan anggun - Anda menghentikannya), dan komputer di belakang mobil Anda terbanting ke kursi di depannya. Satu kaleng penuh Coke tumpah di atas kertas. Bahan makanan di bagian belakang dihancurkan, dan sebagian besar ditutupi dengan kuning telur dan susu. Mobil itu perlu perbaikan dan pembersihan yang serius. (Data hilang)

Semoga Anda memiliki asuransi (Cadangan). Oh ya - karena airbagnya tidak mengembang, Anda mungkin terluka (dipecat, dll).


Tapi tunggu! Adalebihalasan mengapa Anda mungkin ingin menggunakan rescue Exception => e!

Katakanlah Anda adalah mobil itu, dan Anda ingin memastikan airbag mengembang jika mobil melebihi momentum berhenti yang aman.

 begin 
    # do driving stuff
 rescue Exception => e
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
    raise
 end

Ini pengecualian untuk aturan: Anda Exception hanya dapat menangkap jika Anda menaikkan kembali pengecualian . Jadi, aturan yang lebih baik adalah tidak pernah menelan Exception, dan selalu meningkatkan kembali kesalahan.

Namun menambahkan penyelamatan mudah untuk dilupakan dalam bahasa seperti Ruby, dan menempatkan pernyataan penyelamatan tepat sebelum mengangkat kembali masalah terasa sedikit tidak kering. Dan Anda tidak ingin melupakan raisepernyataan itu. Dan jika Anda melakukannya, semoga berhasil mencari kesalahan itu.

Untungnya, Ruby luar biasa, Anda bisa menggunakan ensurekata kunci, yang memastikan kode berjalan. Kata ensurekunci akan menjalankan kode apa pun yang terjadi - jika ada pengecualian, jika tidak ada, satu-satunya pengecualian adalah jika dunia berakhir (atau peristiwa tidak terduga lainnya).

 begin 
    # do driving stuff
 ensure
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
 end

Ledakan! Dan kode itu harus dijalankan. Satu-satunya alasan yang harus Anda gunakan rescue Exception => eadalah jika Anda membutuhkan akses ke pengecualian, atau jika Anda hanya ingin kode dijalankan pada pengecualian. Dan ingatlah untuk menaikkan kembali kesalahan. Setiap saat.

Catatan: Seperti yang ditunjukkan @Niall, pastikan selalu berjalan. Ini bagus karena kadang-kadang program Anda bisa berbohong kepada Anda dan tidak melempar pengecualian, bahkan ketika masalah terjadi. Dengan tugas-tugas penting, seperti menggembungkan kantung udara, Anda harus memastikan itu terjadi apa pun yang terjadi. Karena itu, memeriksa setiap kali mobil berhenti, apakah ada pengecualian atau tidak, adalah ide yang bagus. Meskipun menggembungkan airbag sedikit tugas yang tidak biasa dalam kebanyakan konteks pemrograman, ini sebenarnya cukup umum dengan sebagian besar tugas pembersihan.

Ben Aubin
sumber
12
Hahahaha! Ini jawaban yang bagus. Saya terkejut bahwa tidak ada yang berkomentar. Anda memberikan skenario yang jelas yang membuat semuanya benar-benar dapat dimengerti. Bersulang! :-)
James Milani
@JamesMilani Terima kasih!
Ben Aubin
3
+ 💯 untuk jawaban ini. Seandainya aku bisa lebih baik sekali! 😂
engineerDave
1
Selamat menikmati jawaban Anda!
Atul Vaibhav
3
Jawaban ini datang 4 tahun setelah jawaban diterima dengan sempurna dan benar, dan menjelaskannya kembali dengan skenario yang absurd yang dirancang lebih untuk menghibur daripada realistis. Maaf menjadi buzzkill, tapi ini bukan reddit - lebih penting untuk jawaban yang ringkas dan benar daripada lucu. Juga, bagian tentang ensuresebagai alternatif untuk rescue Exceptionmenyesatkan - contohnya menyiratkan mereka setara, tetapi sebagaimana dinyatakan ensureakan terjadi apakah ada Pengecualian atau tidak, jadi sekarang kantung udara Anda akan mengembang karena Anda melebihi 5 mph meskipun tidak ada yang salah.
Niall
47

Karena ini menangkap semua pengecualian. Tidak mungkin program Anda dapat pulih dari apa pun satunya.

Anda harus menangani hanya pengecualian yang Anda tahu cara memulihkannya. Jika Anda tidak mengantisipasi jenis pengecualian tertentu, jangan menanganinya, crash dengan keras (tulis detail ke log), lalu diagnosa log dan perbaiki kode.

Menelan pengecualian itu buruk, jangan lakukan ini.

Sergio Tulentsev
sumber
10

Itu kasus spesifik aturan bahwa Anda tidak harus menangkap setiap pengecualian Anda tidak tahu bagaimana menangani. Jika Anda tidak tahu cara menanganinya, selalu lebih baik membiarkan bagian lain dari sistem menangkap dan menanganinya.

Russell Borogove
sumber
0

Saya baru saja membaca posting blog yang bagus tentang itu di honeybadger.io :

Pengecualian Ruby vs StandardError: Apa bedanya?

Mengapa Anda tidak harus menyelamatkan Exception

Masalah dengan menyelamatkan Exception adalah bahwa itu benar-benar menyelamatkan setiap pengecualian yang mewarisi dari Exception. Yang mana .... semuanya!

Itu masalah karena ada beberapa pengecualian yang digunakan secara internal oleh Ruby. Mereka tidak ada hubungannya dengan aplikasi Anda, dan menelannya akan menyebabkan hal-hal buruk terjadi.

Berikut adalah beberapa yang besar:

  • SignalException :: Interrupt - Jika Anda menyelamatkan ini, Anda tidak dapat keluar dari aplikasi Anda dengan menekan control-c.

  • ScriptError :: SyntaxError - Menelan kesalahan sintaks berarti bahwa hal-hal seperti menempatkan ("Lupa sesuatu) akan gagal diam-diam.

  • NoMemoryError - Ingin tahu apa yang terjadi ketika program Anda terus berjalan setelah menggunakan semua RAM? Aku juga tidak.

begin
  do_something()
rescue Exception => e
  # Don't do this. This will swallow every single exception. Nothing gets past it. 
end

Saya menduga Anda tidak benar-benar ingin menelan semua pengecualian tingkat sistem ini. Anda hanya ingin menangkap semua kesalahan level aplikasi Anda. Pengecualian menyebabkan kode ANDA.

Untungnya, ada cara mudah untuk ini.

calebkm
sumber