Apa praktik terbaik untuk menangkap dan melempar kembali pengecualian?

156

Haruskah pengecualian yang tertangkap dilemparkan kembali secara langsung, atau haruskah mereka diliputi pengecualian baru?

Yaitu, yang harus saya lakukan ini:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw $e;
}

atau ini:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw new Exception("Exception Message", 1, $e);
}

Jika jawaban Anda adalah untuk melempar secara langsung, harap sarankan penggunaan rantai pengecualian , saya tidak dapat memahami skenario dunia nyata di mana kami menggunakan rantai pengecualian.

Rahul Prasad
sumber

Jawaban:

287

Anda seharusnya tidak menangkap pengecualian kecuali Anda bermaksud melakukan sesuatu yang berarti .

"Sesuatu yang bermakna" mungkin salah satunya:

Menangani pengecualian

Tindakan bermakna yang paling jelas adalah menangani pengecualian, misalnya dengan menampilkan pesan kesalahan dan membatalkan operasi:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    echo "Error while connecting to database!";
    die;
}

Pembalakan atau pembersihan sebagian

Terkadang Anda tidak tahu bagaimana menangani dengan benar pengecualian dalam konteks tertentu; mungkin Anda kurang informasi tentang "gambaran besar", tetapi Anda ingin mencatat kegagalan sedekat mungkin dengan titik di mana hal itu terjadi. Dalam hal ini, Anda mungkin ingin menangkap, mencatat, dan melemparkan kembali:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    logException($e); // does something
    throw $e;
}

Skenario terkait adalah di mana Anda berada di tempat yang tepat untuk melakukan pembersihan untuk operasi yang gagal, tetapi tidak memutuskan bagaimana kegagalan harus ditangani di tingkat atas. Dalam versi PHP sebelumnya ini akan diimplementasikan sebagai

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
catch (Exception $e) {
    $connect->disconnect(); // we don't want to keep the connection open anymore
    throw $e; // but we also don't know how to respond to the failure
}

PHP 5.5 telah memperkenalkan finallykata kunci, jadi untuk skenario pembersihan sekarang ada cara lain untuk mendekati ini. Jika kode pembersihan perlu dijalankan tidak peduli apa yang terjadi (mis. Baik pada kesalahan maupun kesuksesan) sekarang mungkin untuk melakukan ini sementara secara transparan memungkinkan segala pengecualian yang dilemparkan untuk disebarkan:

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
finally {
    $connect->disconnect(); // no matter what
}

Abstraksi kesalahan (dengan chaining pengecualian)

Kasus ketiga adalah di mana Anda ingin mengelompokkan banyak kegagalan yang mungkin secara logis di bawah payung yang lebih besar. Contoh untuk pengelompokan logis:

class ComponentInitException extends Exception {
    // public constructors etc as in Exception
}

class Component {
    public function __construct() {
        try {
            $connect = new CONNECT($db, $user, $password, $driver, $host);
        }
        catch (Exception $e) {
            throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
        }
    }
}

Dalam hal ini, Anda tidak ingin pengguna Componenttahu bahwa itu diimplementasikan menggunakan koneksi database (mungkin Anda ingin menjaga opsi Anda terbuka dan menggunakan penyimpanan berbasis file di masa depan). Jadi spesifikasi Anda untuk Componentakan mengatakan bahwa "dalam kasus kegagalan inisialisasi, ComponentInitExceptionakan dibuang". Hal ini memungkinkan konsumen Componentuntuk menangkap pengecualian dari tipe yang diharapkan sementara juga memungkinkan kode debug untuk mengakses semua detail (tergantung pada implementasi) .

Memberikan konteks yang lebih kaya (dengan chaining pengecualian)

Akhirnya, ada beberapa kasus di mana Anda mungkin ingin memberikan lebih banyak konteks untuk pengecualian. Dalam hal ini masuk akal untuk membungkus pengecualian di yang lain yang menampung lebih banyak informasi tentang apa yang Anda coba lakukan ketika kesalahan terjadi. Sebagai contoh:

class FileOperation {
    public static function copyFiles() {
        try {
            $copier = new FileCopier(); // the constructor may throw

            // this may throw if the files do no not exist
            $copier->ensureSourceFilesExist();

            // this may throw if the directory cannot be created
            $copier->createTargetDirectory();

            // this may throw if copying a file fails
            $copier->performCopy();
        }
        catch (Exception $e) {
            throw new Exception("Could not perform copy operation.", 0, $e);
        }
    }
}

Kasus ini mirip dengan yang di atas (dan contohnya mungkin bukan yang terbaik yang bisa muncul), tetapi menggambarkan titik memberikan lebih banyak konteks: jika pengecualian dilemparkan, ia memberi tahu kita bahwa salinan file gagal. Tetapi mengapa itu gagal? Informasi ini disediakan dalam pengecualian yang dibungkus (yang mungkin ada lebih dari satu tingkat jika contohnya jauh lebih rumit).

Nilai melakukan ini diilustrasikan jika Anda berpikir tentang skenario di mana misalnya membuat UserProfileobjek menyebabkan file akan disalin karena profil pengguna disimpan dalam file dan mendukung semantik transaksi: Anda dapat "membatalkan" perubahan karena hanya dilakukan pada Salin profil sampai Anda komit.

Dalam hal ini, jika Anda melakukannya

try {
    $profile = UserProfile::getInstance();
}

dan akibatnya menangkap kesalahan pengecualian "Direktori target tidak dapat dibuat", Anda berhak untuk dibingungkan. Membungkus pengecualian "inti" ini di lapisan pengecualian lain yang menyediakan konteks akan membuat kesalahan lebih mudah untuk ditangani ("Membuat salinan profil gagal" -> "Operasi penyalinan file gagal" -> "Direktori target tidak dapat dibuat").

Jon
sumber
Saya hanya setuju dengan 2 alasan terakhir: 1 / menangani pengecualian: Anda tidak boleh melakukannya pada tingkat ini, 2 / masuk atau membersihkan: gunakan akhirnya dan catat pengecualian di atas datalayer Anda
remi bourgarel
1
@remi: kecuali bahwa PHP tidak mendukung finallykonstruksi (setidaknya belum) ... Jadi itu keluar, yang berarti kita harus menggunakan hal-hal kotor seperti ini ...
ircmaxell
@remibourgarel: 1: Itu hanya contoh. Tentu saja Anda tidak boleh melakukannya di level ini, tetapi jawabannya sudah cukup lama. 2: Seperti yang dikatakan @ircmaxell, tidak ada finallydi PHP.
Jon
3
Akhirnya, PHP 5.5 sekarang mengimplementasikan akhirnya.
OCDev
12
Ada alasan saya pikir Anda melewatkan dari daftar Anda di sini - Anda mungkin tidak dapat mengetahui apakah Anda dapat menangani pengecualian sampai Anda menangkapnya dan memiliki kesempatan untuk memeriksanya. Misalnya, pembungkus untuk API tingkat rendah yang menggunakan kode kesalahan (dan memiliki zillion di antaranya) mungkin memiliki kelas pengecualian tunggal yang dilontarkannya instance untuk setiap kesalahan, dengan error_codeproperti yang dapat diperiksa untuk mendapatkan kesalahan yang mendasarinya. kode. Jika Anda hanya mampu menangani beberapa kesalahan tersebut secara berarti, maka Anda mungkin ingin menangkap, memeriksa, dan jika Anda tidak dapat menangani kesalahan - rethrow.
Mark Amery
37

Yah, ini semua tentang menjaga abstraksi. Jadi saya sarankan menggunakan rantai pengecualian untuk melempar langsung. Sejauh mengapa, izinkan saya menjelaskan konsep abstraksi bocor

Katakanlah Anda sedang membangun model. Model ini seharusnya mengabstraksi semua ketekunan dan validasi data dari sisa aplikasi. Jadi sekarang apa yang terjadi ketika Anda mendapatkan kesalahan basis data? Jika Anda menata ulang DatabaseQueryException, Anda membocorkan abstraksi. Untuk memahami mengapa, pikirkan abstraksi sejenak. Anda tidak peduli bagaimana model menyimpan data, hanya saja itu terjadi. Demikian juga Anda tidak peduli apa yang salah dalam sistem yang mendasari model, hanya bahwa Anda tahu ada sesuatu yang salah, dan kira-kira apa yang salah.

Jadi dengan memikirkan kembali DatabaseQueryException, Anda membocorkan abstraksi dan memerlukan kode panggilan untuk memahami semantik dari apa yang terjadi di bawah model. Sebagai gantinya, buatlah obat generik ModelStorageException, dan bungkus bagian DatabaseQueryExceptiondalamnya. Dengan begitu, kode panggilan Anda masih dapat mencoba menangani kesalahan secara semantik, tetapi tidak masalah teknologi yang mendasari Model karena Anda hanya mengekspos kesalahan dari lapisan abstraksi itu. Bahkan lebih baik, karena Anda membungkus pengecualian, jika itu gelembung sepanjang jalan dan perlu dicatat, Anda dapat melacak ke pengecualian akar dilemparkan (berjalan rantai) sehingga Anda masih memiliki semua informasi debug yang Anda butuhkan!

Jangan hanya menangkap dan memikirkan kembali pengecualian yang sama kecuali Anda perlu melakukan beberapa post-processing. Tapi blok seperti } catch (Exception $e) { throw $e; }itu tidak ada gunanya. Tetapi Anda dapat membungkus kembali pengecualian untuk beberapa keuntungan abstraksi yang signifikan.

ircmaxell
sumber
2
Jawaban yang bagus Tampaknya beberapa orang di sekitar Stack Overflow (berdasarkan jawaban dll) agak salah menggunakannya.
James
8

IMHO, menangkap Pengecualian hanya untuk rethrow tidak berguna . Dalam hal ini, jangan menangkapnya, dan biarkan metode yang disebut sebelumnya menanganinya (alias metode yang 'atas' di tumpukan panggilan) .

Jika Anda mengolahnya kembali, merantai pengecualian yang tertangkap menjadi yang baru yang akan Anda lempar jelas merupakan praktik yang baik, karena akan menyimpan informasi yang terkandung di dalamnya. Namun, memikirkan kembali itu hanya berguna jika Anda menambahkan beberapa informasi atau menangani sesuatu dengan pengecualian yang ditangkap, mungkin itu beberapa konteks, nilai, logging, membebaskan sumber daya, apa pun.

Sebuah cara untuk menambahkan beberapa informasi untuk memperpanjang Exceptionkelas, untuk memiliki pengecualian seperti NullParameterException, DatabaseException, dll Terlebih lagi, ini memungkinkan developper untuk hanya menangkap beberapa pengecualian bahwa ia dapat menangani. Misalnya, seseorang hanya dapat menangkap DatabaseExceptiondan mencoba untuk memecahkan apa yang menyebabkannya Exception, seperti menghubungkan kembali ke databse.

Clement Herreman
sumber
2
Ini tidak sia-sia, ada kalanya Anda perlu melakukan sesuatu dengan perkecualian mengatakan dalam fungsi yang melemparnya dan kemudian melemparkannya kembali agar tangkapan yang lebih tinggi melakukan sesuatu yang lain. Dalam salah satu proyek yang sedang saya kerjakan, kadang-kadang kami menangkap pengecualian dalam metode tindakan, menampilkan pemberitahuan ramah kepada pengguna dan kemudian melemparkannya kembali sehingga blok tangkap coba lebih jauh dalam kode dapat menangkapnya lagi untuk mencatat kesalahan ke sebuah log.
MitMaro
1
Jadi seperti yang saya katakan, Anda menambahkan beberapa informasi ke pengecualian (menampilkan pemberitahuan, mencatatnya). Anda tidak hanya memikirkan kembali seperti pada contoh OP.
Clement Herreman
2
Nah, Anda bisa hanya memikirkan kembali jika Anda perlu menutup sumber daya, tetapi tidak memiliki informasi tambahan untuk ditambahkan. Saya setuju itu bukan hal terbersih di dunia, tapi itu tidak mengerikan
ircmaxell
2
@ircmaxell Setuju, diedit untuk mencerminkan bahwa tidak ada gunanya hanya jika Anda tidak melakukan apa pun kecuali melakukan rethrowing
Clement Herreman
1
Yang penting adalah Anda kehilangan file dan / atau informasi baris tempat pengecualian awalnya dilemparkan dengan melemparkannya kembali. Jadi biasanya lebih baik menyisipkan yang baru dan meneruskan yang lama, seperti pada contoh ke-2 pertanyaan. Kalau tidak, itu hanya akan menunjuk ke blok tangkap, membuat Anda menebak apa masalah sebenarnya.
DanMan
2

Anda harus melihatnya Pengecualian Praktik Terbaik di PHP 5.3

Penanganan pengecualian di PHP bukanlah fitur baru. Di tautan berikut, Anda akan melihat dua fitur baru di PHP 5.3 berdasarkan pengecualian. Yang pertama adalah pengecualian bersarang dan yang kedua adalah serangkaian tipe pengecualian baru yang ditawarkan oleh ekstensi SPL (yang sekarang merupakan ekstensi inti dari runtime PHP). Kedua fitur baru ini telah masuk ke dalam buku praktik terbaik terbaik dan layak untuk diperiksa secara terperinci.

http://ralphschindler.com/2010/09/15/exception-best-practices-in-php-5-3

HMagdy
sumber
1

Anda biasanya berpikir seperti ini.

Kelas mungkin melempar banyak jenis pengecualian yang tidak cocok. Jadi Anda membuat kelas pengecualian untuk kelas atau tipe kelas itu dan membuangnya.

Jadi kode yang menggunakan kelas hanya harus menangkap satu jenis pengecualian.

Ólafur Waage
sumber
1
Hai, bisakah Anda memberikan lebih banyak detail atau tautan di mana saya dapat membaca lebih lanjut tentang pendekatan ini.
Rahul Prasad