Saat kita mengembangkan sesuatu, kita sering kali memerlukan kelas kesalahan kita sendiri untuk mencerminkan hal-hal spesifik yang mungkin salah dalam tugas kita. Untuk kesalahan dalam operasi jaringan kita mungkin memerlukan HttpError
, untuk operasi database DbError
, untuk operasi pencarian NotFoundError
dan seterusnya.
Kesalahan kita harus mendukung properti kesalahan dasar seperti message
, name
dan, sebaiknya, stack
. Namun mereka juga mungkin memiliki properti lain, misalnya objek HttpError
mungkin memiliki properti statusCode
dengan nilai seperti 404
atau 403
atau 500
.
JavaScript memungkinkan penggunaan throw
dengan argumen apa pun, jadi secara teknis kelas kesalahan khusus kami tidak perlu mewarisi dari Error
. Tetapi jika kita mewarisi, maka dimungkinkan untuk menggunakan obj instanceof Error
untuk mengidentifikasi objek kesalahan. Jadi lebih baik mewarisinya.
Seiring berkembangnya aplikasi, kesalahan kita secara alami membentuk hierarki. Misalnya, HttpTimeoutError
mungkin mewarisi dari HttpError
, dan seterusnya.
Sebagai contoh, mari kita pertimbangkan fungsi readUser(json)
yang harus membaca JSON dengan data pengguna.
Berikut ini contoh tampilan json
yang valid:
biarkan json = `{ "nama": "John", "umur": 30 }`;
Secara internal, kami akan menggunakan JSON.parse
. Jika menerima json
yang salah format, maka SyntaxError
akan muncul. Namun meskipun json
benar secara sintaksis, bukan berarti json tersebut adalah pengguna yang valid, bukan? Ini mungkin kehilangan data yang diperlukan. Misalnya, properti tersebut mungkin tidak memiliki properti name
dan age
yang penting bagi pengguna kami.
Fungsi kami readUser(json)
tidak hanya akan membaca JSON, tetapi juga memeriksa (“memvalidasi”) data. Jika tidak ada kolom yang wajib diisi, atau formatnya salah, maka itu adalah kesalahan. Dan itu bukan SyntaxError
, karena datanya benar secara sintaksis, melainkan jenis kesalahan lain. Kami akan menyebutnya ValidationError
dan membuat kelas untuk itu. Kesalahan semacam itu juga harus membawa informasi tentang bidang yang melanggar.
Kelas ValidationError
kita harus mewarisi dari kelas Error
.
Kelas Error
sudah ada di dalamnya, namun berikut perkiraan kodenya sehingga kami dapat memahami apa yang kami perluas:
// "Kode semu" untuk kelas Error bawaan yang ditentukan oleh JavaScript itu sendiri kelas Kesalahan { konstruktor(pesan) { this.pesan = pesan; this.nama = "Kesalahan"; // (nama berbeda untuk kelas kesalahan bawaan yang berbeda) this.stack = <tumpukan panggilan>; // non-standar, tetapi sebagian besar lingkungan mendukungnya } }
Sekarang mari kita mewarisi ValidationError
darinya dan mencobanya:
kelas ValidationError memperluas Kesalahan { konstruktor(pesan) { super(pesan); // (1) this.nama = "ValidationError"; // (2) } } tes fungsi() { throw new ValidationError("Ups!"); } mencoba { tes(); } tangkapan(salah) { alert(err.pesan); // Ups! alert(err.nama); // Kesalahan Validasi alert(err.stack); // daftar panggilan bersarang dengan nomor saluran untuk masing-masing panggilan }
Harap diperhatikan: pada baris (1)
kita memanggil konstruktor induk. JavaScript mengharuskan kita memanggil super
di konstruktor anak, jadi itu wajib. Konstruktor induk menyetel properti message
.
Konstruktor induk juga menyetel properti name
menjadi "Error"
, jadi pada baris (2)
kita menyetel ulang ke nilai yang benar.
Mari kita coba menggunakannya di readUser(json)
:
kelas ValidationError memperluas Kesalahan { konstruktor(pesan) { super(pesan); this.nama = "ValidationError"; } } // Penggunaan fungsi bacaPengguna(json) { biarkan pengguna = JSON.parse(json); if (!pengguna.usia) { throw new ValidationError("Tidak ada kolom: umur"); } if (!nama pengguna) { throw new ValidationError("Tidak ada kolom: nama"); } pengguna kembali; } // Contoh kerja dengan try..catch mencoba { biarkan pengguna = readUser('{ "usia": 25 }'); } menangkap (salah) { jika (err instanceof ValidationError) { alert("Data tidak valid: "+err.pesan); // Data tidak valid: Tidak ada kolom: nama } else if (err instanceof SyntaxError) { // (*) alert("Kesalahan Sintaks JSON: "+err.message); } kalau tidak { membuang kesalahan; // kesalahan tidak diketahui, tampilkan kembali (**) } }
Blok try..catch
pada kode di atas menangani ValidationError
dan SyntaxError
bawaan dari JSON.parse
.
Silakan lihat bagaimana kami menggunakan instanceof
untuk memeriksa jenis kesalahan spesifik di baris (*)
.
Kita juga bisa melihat err.name
, seperti ini:
// ... // dari pada (err instance dari SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ...
Versi instanceof
jauh lebih baik, karena di masa depan kita akan memperluas ValidationError
, membuat subtipe, seperti PropertyRequiredError
. Dan pemeriksaan instanceof
akan terus berfungsi untuk kelas pewarisan baru. Jadi itu adalah bukti masa depan.
Penting juga bahwa jika catch
menemui kesalahan yang tidak diketahui, maka ia akan menampilkannya kembali di baris (**)
. Blok catch
hanya mengetahui cara menangani kesalahan validasi dan sintaksis, jenis kesalahan lainnya (yang disebabkan oleh kesalahan ketik kode atau alasan lain yang tidak diketahui) akan gagal.
Kelas ValidationError
sangat umum. Banyak hal yang mungkin salah. Properti mungkin tidak ada atau formatnya salah (seperti nilai string untuk age
, bukan angka). Mari kita buat kelas yang lebih konkrit PropertyRequiredError
, tepatnya untuk properti yang tidak ada. Ini akan membawa informasi tambahan tentang properti yang hilang.
kelas ValidationError memperluas Kesalahan { konstruktor(pesan) { super(pesan); this.nama = "ValidationError"; } } kelas PropertyRequiredError memperluas ValidationError { konstruktor(properti) { super("Tidak ada properti: " + properti); this.name = "PropertyRequiredError"; this.property = properti; } } // Penggunaan fungsi bacaPengguna(json) { biarkan pengguna = JSON.parse(json); if (!pengguna.usia) { melempar PropertyRequiredError baru("usia"); } if (!nama pengguna) { melempar PropertyRequiredError baru("nama"); } pengguna kembali; } // Contoh kerja dengan try..catch mencoba { biarkan pengguna = readUser('{ "usia": 25 }'); } menangkap (salah) { jika (err instanceof ValidationError) { alert("Data tidak valid: "+err.pesan); // Data tidak valid: Tidak ada properti: nama alert(err.nama); // PropertyRequiredError alert(err.properti); // nama } else if (err instanceof SyntaxError) { alert("Kesalahan Sintaks JSON: "+err.message); } kalau tidak { membuang kesalahan; // kesalahan tidak diketahui, tampilkan kembali } }
Kelas baru PropertyRequiredError
mudah digunakan: kita hanya perlu meneruskan nama properti: new PropertyRequiredError(property)
. message
yang dapat dibaca manusia dihasilkan oleh konstruktor.
Harap dicatat bahwa this.name
di konstruktor PropertyRequiredError
sekali lagi ditetapkan secara manual. Itu mungkin sedikit membosankan – untuk menetapkan this.name = <class name>
di setiap kelas kesalahan khusus. Kita dapat menghindarinya dengan membuat kelas “kesalahan dasar” kita sendiri yang menetapkan this.name = this.constructor.name
. Dan kemudian mewarisi semua kesalahan khusus kami darinya.
Sebut saja MyError
.
Berikut kode dengan MyError
dan kelas kesalahan khusus lainnya, disederhanakan:
kelas MyError memperluas Kesalahan { konstruktor(pesan) { super(pesan); ini.nama = ini.konstruktor.nama; } } kelas ValidationError memperluas MyError {} kelas PropertyRequiredError memperluas ValidationError { konstruktor(properti) { super("Tidak ada properti: " + properti); this.property = properti; } } // nama benar peringatan( new PropertyRequiredError("bidang").nama ); // PropertyRequiredError
Sekarang kesalahan khusus jauh lebih pendek, terutama ValidationError
, karena kita menghilangkan baris "this.name = ..."
di konstruktor.
Tujuan dari fungsi readUser
pada kode di atas adalah “untuk membaca data pengguna”. Mungkin ada berbagai jenis kesalahan dalam prosesnya. Saat ini kami memiliki SyntaxError
dan ValidationError
, tetapi di masa depan fungsi readUser
mungkin berkembang dan mungkin menghasilkan jenis kesalahan lainnya.
Kode yang memanggil readUser
harus menangani kesalahan ini. Saat ini ia menggunakan beberapa if
di blok catch
, yang memeriksa kelas dan menangani kesalahan yang diketahui dan menampilkan kembali kesalahan yang tidak diketahui.
Skemanya seperti ini:
mencoba { ... readUser() // sumber kesalahan potensial ... } menangkap (salah) { jika (err instanceof ValidationError) { // menangani kesalahan validasi } else if (err instanceof SyntaxError) { // menangani kesalahan sintaksis } kalau tidak { membuang kesalahan; // kesalahan tidak diketahui, tampilkan kembali } }
Pada kode di atas kita dapat melihat dua jenis kesalahan, namun bisa lebih banyak lagi.
Jika fungsi readUser
menghasilkan beberapa jenis kesalahan, maka kita harus bertanya pada diri sendiri: apakah kita benar-benar ingin memeriksa semua jenis kesalahan satu per satu setiap saat?
Seringkali jawabannya adalah “Tidak”: kami ingin menjadi “satu tingkat di atas semua itu”. Kami hanya ingin tahu apakah ada “kesalahan pembacaan data” – mengapa sebenarnya hal itu terjadi sering kali tidak relevan (pesan kesalahan menjelaskannya). Atau, lebih baik lagi, kami ingin memiliki cara untuk mendapatkan detail kesalahan, namun hanya jika diperlukan.
Teknik yang kami uraikan di sini disebut “pengecualian pembungkusan”.
Kita akan membuat kelas baru ReadError
untuk mewakili kesalahan umum “pembacaan data”.
Fungsi readUser
akan menangkap kesalahan pembacaan data yang terjadi di dalamnya, seperti ValidationError
dan SyntaxError
, dan sebagai gantinya menghasilkan ReadError
.
Objek ReadError
akan menyimpan referensi ke kesalahan asli di properti cause
.
Maka kode yang memanggil readUser
hanya perlu memeriksa ReadError
, bukan setiap jenis kesalahan pembacaan data. Dan jika memerlukan rincian lebih lanjut mengenai suatu kesalahan, ia dapat memeriksa properti cause
.
Berikut kode yang mendefinisikan ReadError
dan mendemonstrasikan penggunaannya di readUser
dan try..catch
:
kelas ReadError memperluas Kesalahan { konstruktor(pesan, penyebab) { super(pesan); this.cause = penyebab; this.nama = 'ReadError'; } } kelas ValidationError memperluas Kesalahan { /*...*/ } kelas PropertyRequiredError memperluas ValidationError { /* ... */ } fungsi validasiPengguna(pengguna) { if (!pengguna.usia) { melempar PropertyRequiredError baru("usia"); } if (!nama pengguna) { melempar PropertyRequiredError baru("nama"); } } fungsi bacaPengguna(json) { biarkan pengguna; mencoba { pengguna = JSON.parse(json); } menangkap (salah) { jika (err instanceof SyntaxError) { melempar ReadError baru("Kesalahan Sintaks", err); } kalau tidak { membuang kesalahan; } } mencoba { validasiPengguna(pengguna); } menangkap (salah) { jika (err instanceof ValidationError) { throw new ReadError("Kesalahan Validasi", err); } kalau tidak { membuang kesalahan; } } } mencoba { readUser('{json buruk}'); } tangkapan (e) { jika (e instanceof ReadError) { peringatan(e); // Kesalahan asli: SyntaxError: Token b tak terduga di JSON pada posisi 1 alert("Kesalahan awal: "+e.cause); } kalau tidak { melempar e; } }
Dalam kode di atas, readUser
berfungsi persis seperti yang dijelaskan – menangkap kesalahan sintaksis dan validasi dan menampilkan kesalahan ReadError
(kesalahan yang tidak diketahui ditampilkan kembali seperti biasa).
Jadi kode luar memeriksa instanceof ReadError
dan hanya itu. Tidak perlu mencantumkan semua jenis kesalahan yang mungkin terjadi.
Pendekatan ini disebut “membungkus pengecualian”, karena kita mengambil pengecualian “tingkat rendah” dan “membungkusnya” ke dalam ReadError
yang lebih abstrak. Ini banyak digunakan dalam pemrograman berorientasi objek.
Kita dapat mewarisi dari Error
dan kelas kesalahan bawaan lainnya secara normal. Kita hanya perlu menjaga name
propertinya dan jangan lupa menelepon super
.
Kita dapat menggunakan instanceof
untuk memeriksa kesalahan tertentu. Ini juga berfungsi dengan warisan. Namun terkadang kita memiliki objek kesalahan yang berasal dari perpustakaan pihak ketiga dan tidak ada cara mudah untuk mendapatkan kelasnya. Kemudian properti name
dapat digunakan untuk pemeriksaan tersebut.
Membungkus pengecualian adalah teknik yang tersebar luas: suatu fungsi menangani pengecualian tingkat rendah dan menciptakan kesalahan tingkat tinggi alih-alih berbagai kesalahan tingkat rendah. Pengecualian tingkat rendah terkadang menjadi properti objek tersebut seperti err.cause
pada contoh di atas, namun hal tersebut tidak sepenuhnya diwajibkan.
pentingnya: 5
Buat kelas FormatError
yang mewarisi kelas SyntaxError
bawaan.
Itu harus mendukung properti message
, name
dan stack
.
Contoh penggunaan:
biarkan err = new FormatError("kesalahan format"); alert( err.pesan ); // kesalahan pemformatan alert( err.nama ); // Kesalahan Format peringatan( err.stack ); // tumpukan peringatan( err contoh FormatError ); // BENAR peringatan( err instanceof SyntaxError ); // benar (karena mewarisi dari SyntaxError)
kelas FormatError memperluas SyntaxError { konstruktor(pesan) { super(pesan); ini.nama = ini.konstruktor.nama; } } biarkan err = new FormatError("kesalahan format"); alert( err.pesan ); // kesalahan pemformatan alert( err.nama ); // Kesalahan Format peringatan( err.stack ); // tumpukan peringatan( err instanceof SyntaxError ); // BENAR