Sehebat apapun kita dalam pemrograman, terkadang skrip kita mengalami kesalahan. Hal ini mungkin terjadi karena kesalahan kami, masukan pengguna yang tidak terduga, respons server yang salah, dan ribuan alasan lainnya.
Biasanya, skrip “mati” (segera berhenti) jika terjadi kesalahan, mencetaknya ke konsol.
Namun ada konstruksi sintaksis try...catch
yang memungkinkan kita untuk "menangkap" kesalahan sehingga skrip, alih-alih mati, dapat melakukan sesuatu yang lebih masuk akal.
Konstruksi try...catch
memiliki dua blok utama: try
, dan kemudian catch
:
mencoba { // kode... } menangkap (salah) { // penanganan kesalahan }
Cara kerjanya seperti ini:
Pertama, kode di try {...}
dijalankan.
Jika tidak ada kesalahan, maka catch (err)
diabaikan: eksekusi mencapai akhir try
dan melanjutkan, melewatkan catch
.
Jika terjadi kesalahan, maka eksekusi try
dihentikan, dan kontrol mengalir ke awal catch (err)
. Variabel err
(kita dapat menggunakan nama apa pun untuknya) akan berisi objek error dengan rincian tentang apa yang terjadi.
Jadi, kesalahan di dalam blok try {...}
tidak mematikan skrip – kita memiliki kesempatan untuk menanganinya di catch
.
Mari kita lihat beberapa contoh.
Contoh tanpa kesalahan: menampilkan alert
(1)
dan (2)
:
mencoba { alert('Percobaan dimulai'); // (1) <-- // ...tidak ada kesalahan di sini alert('Akhir percobaan berjalan'); // (2) <-- } menangkap (salah) { alert('Tangkapan diabaikan, karena tidak ada kesalahan'); // (3) }
Contoh dengan kesalahan: menunjukkan (1)
dan (3)
:
mencoba { alert('Percobaan dimulai'); // (1) <-- lalala; // kesalahan, variabel tidak ditentukan! alert('Akhir percobaan (tidak pernah tercapai)'); // (2) } menangkap (salah) { alert(`Terjadi kesalahan!`); // (3) <-- }
try...catch
hanya berfungsi untuk kesalahan runtime
Agar try...catch
berfungsi, kode harus dapat dijalankan. Dengan kata lain, itu harus berupa JavaScript yang valid.
Ini tidak akan berfungsi jika kodenya salah secara sintaksis, misalnya kode tersebut memiliki kurung kurawal yang tidak cocok:
mencoba { {{{{{{{{{{{{ } menangkap (salah) { alert("Mesin tidak dapat memahami kode ini, kode ini tidak valid"); }
Mesin JavaScript pertama-tama membaca kodenya, lalu menjalankannya. Kesalahan yang terjadi pada fase membaca disebut kesalahan “parse-time” dan tidak dapat dipulihkan (dari dalam kode tersebut). Itu karena mesin tidak dapat memahami kodenya.
Jadi, try...catch
hanya dapat menangani kesalahan yang terjadi pada kode yang valid. Kesalahan seperti ini disebut “kesalahan runtime” atau, terkadang, “pengecualian”.
try...catch
bekerja secara serempak
Jika pengecualian terjadi dalam kode "terjadwal", seperti di setTimeout
, maka try...catch
tidak akan menangkapnya:
mencoba { setTimeout(fungsi() { tidak adaVariabel Tersebut; // skrip akan mati di sini }, 1000); } menangkap (salah) { alert("Tidak bisa"); }
Itu karena fungsinya sendiri dieksekusi kemudian, ketika mesin telah meninggalkan konstruksi try...catch
.
Untuk menangkap pengecualian di dalam fungsi terjadwal, try...catch
harus ada di dalam fungsi itu:
setTimeout(fungsi() { mencoba { tidak adaVariabel Tersebut; // coba...tangkap menangani kesalahannya! } menangkap { alert("kesalahan tertangkap di sini!"); } }, 1000);
Ketika kesalahan terjadi, JavaScript menghasilkan objek yang berisi detailnya. Objek tersebut kemudian diteruskan sebagai argumen ke catch
:
mencoba { // ... } catch (err) { // <-- "objek kesalahan", bisa menggunakan kata lain selain err // ... }
Untuk semua kesalahan bawaan, objek kesalahan memiliki dua properti utama:
name
Nama kesalahan. Misalnya, untuk variabel yang tidak ditentukan yaitu "ReferenceError"
.
message
Pesan tekstual tentang detail kesalahan.
Ada properti non-standar lainnya yang tersedia di sebagian besar lingkungan. Salah satu yang paling banyak digunakan dan didukung adalah:
stack
Tumpukan panggilan saat ini: string berisi informasi tentang urutan panggilan bersarang yang menyebabkan kesalahan. Digunakan untuk tujuan debugging.
Misalnya:
mencoba { lalala; // kesalahan, variabel tidak ditentukan! } menangkap (salah) { alert(err.nama); // Kesalahan Referensi alert(err.pesan); // lalala tidak didefinisikan alert(err.stack); // Kesalahan Referensi: lalala tidak ditentukan di (...tumpukan panggilan) // Bisa juga menampilkan error secara keseluruhan // Kesalahan diubah menjadi string sebagai "nama: pesan" waspada(salah); //ReferenceError: lalala tidak ditentukan }
Tambahan baru-baru ini
Ini adalah tambahan terbaru pada bahasa ini. Browser lama mungkin memerlukan polyfill.
Jika kita tidak memerlukan detail kesalahan, catch
dapat menghilangkannya:
mencoba { // ... } catch { // <-- tanpa (err) // ... }
Mari jelajahi kasus penggunaan try...catch
di kehidupan nyata.
Seperti yang telah kita ketahui, JavaScript mendukung metode JSON.parse(str) untuk membaca nilai yang dikodekan JSON.
Biasanya digunakan untuk memecahkan kode data yang diterima melalui jaringan, dari server atau sumber lain.
Kami menerimanya dan memanggil JSON.parse
seperti ini:
biarkan json = '{"nama":"John", "umur": 30}'; // data dari server biarkan pengguna = JSON.parse(json); // mengonversi representasi teks menjadi objek JS // sekarang pengguna adalah objek dengan properti dari string peringatan( nama pengguna ); // Yohanes peringatan( pengguna.usia ); // 30
Anda dapat menemukan informasi lebih detail tentang JSON di metode JSON, bab toJSON.
Jika format json
salah, JSON.parse
menghasilkan kesalahan, sehingga skrip “mati”.
Apakah kita harus puas dengan hal itu? Tentu saja tidak!
Dengan cara ini, jika ada yang salah dengan datanya, pengunjung tidak akan pernah mengetahuinya (kecuali mereka membuka konsol pengembang). Dan orang-orang sangat tidak suka jika sesuatu “mati begitu saja” tanpa pesan kesalahan apa pun.
Mari gunakan try...catch
untuk menangani kesalahan:
misalkan json = "{ json buruk }"; mencoba { biarkan pengguna = JSON.parse(json); // <-- ketika terjadi kesalahan... peringatan( nama pengguna ); // tidak berfungsi } menangkap (salah) { // ...eksekusinya melompat ke sini alert("Mohon maaf, data ada error, kami akan coba request sekali lagi." ); alert( err.nama ); alert( err.pesan ); }
Di sini kita menggunakan blok catch
hanya untuk menampilkan pesan, namun kita dapat melakukan lebih banyak lagi: mengirim permintaan jaringan baru, menyarankan alternatif kepada pengunjung, mengirim informasi tentang kesalahan ke fasilitas logging,…. Semuanya jauh lebih baik dari sekedar mati.
Bagaimana jika json
benar secara sintaksis, tetapi tidak memiliki properti name
yang diperlukan?
Seperti ini:
misalkan json = '{ "umur": 30 }'; // data tidak lengkap mencoba { biarkan pengguna = JSON.parse(json); // <-- tidak ada kesalahan peringatan( nama pengguna ); // tidak ada nama! } menangkap (salah) { alert("tidak dijalankan"); }
Di sini JSON.parse
berjalan normal, tetapi tidak adanya name
sebenarnya merupakan kesalahan bagi kami.
Untuk menyatukan penanganan kesalahan, kita akan menggunakan operator throw
.
Operator throw
menghasilkan kesalahan.
Sintaksnya adalah:
melempar <objek kesalahan>
Secara teknis, kita bisa menggunakan apa saja sebagai objek kesalahan. Itu bahkan mungkin primitif, seperti angka atau string, tetapi lebih baik menggunakan objek, lebih disukai dengan properti name
dan message
(agar tetap kompatibel dengan kesalahan bawaan).
JavaScript memiliki banyak konstruktor bawaan untuk kesalahan standar: Error
, SyntaxError
, ReferenceError
, TypeError
dan lain-lain. Kita juga bisa menggunakannya untuk membuat objek kesalahan.
Sintaksnya adalah:
biarkan error = new Error(pesan); // atau biarkan error = new SyntaxError(pesan); biarkan kesalahan = Referensi Baru(pesan); // ...
Untuk kesalahan bawaan (bukan untuk objek apa pun, hanya untuk kesalahan), properti name
sama persis dengan nama konstruktor. Dan message
diambil dari argumen tersebut.
Misalnya:
let error = new Error("Hal-hal terjadi o_O"); alert(kesalahan.nama); // Kesalahan alert(kesalahan.pesan); // Banyak hal terjadi o_O
Mari kita lihat jenis kesalahan apa yang dihasilkan JSON.parse
:
mencoba { JSON.parse("{ json buruk o_O }"); } menangkap (salah) { alert(err.nama); // Kesalahan Sintaks alert(err.pesan); // Token b yang tidak terduga di JSON pada posisi 2 }
Seperti yang bisa kita lihat, itu adalah SyntaxError
.
Dan dalam kasus kami, tidak adanya name
adalah kesalahan, karena pengguna harus memiliki name
.
Jadi mari kita buang:
misalkan json = '{ "umur": 30 }'; // data tidak lengkap mencoba { biarkan pengguna = JSON.parse(json); // <-- tidak ada kesalahan if (!nama pengguna) { throw new SyntaxError("Data tidak lengkap: tidak ada nama"); // (*) } peringatan( nama pengguna ); } menangkap (salah) { alert("Kesalahan JSON: " + err.pesan ); // Kesalahan JSON: Data tidak lengkap: tidak ada nama }
Pada baris (*)
, operator throw
menghasilkan SyntaxError
dengan message
yang diberikan, sama seperti JavaScript menghasilkannya sendiri. Eksekusi try
segera berhenti dan aliran kontrol melompat ke catch
.
Sekarang catch
menjadi satu tempat untuk semua penanganan kesalahan: baik untuk JSON.parse
maupun kasus lainnya.
Pada contoh di atas kita menggunakan try...catch
untuk menangani data yang salah. Tetapi mungkinkah terjadi kesalahan tak terduga lainnya dalam blok try {...}
? Seperti kesalahan pemrograman (variabel tidak ditentukan) atau yang lainnya, bukan hanya masalah “data yang salah” ini.
Misalnya:
misalkan json = '{ "umur": 30 }'; // data tidak lengkap mencoba { pengguna = JSON.parse(json); // <-- lupa meletakkan "biarkan" sebelum pengguna // ... } menangkap (salah) { alert("Kesalahan JSON: "+err); // Kesalahan JSON: Kesalahan Referensi: pengguna tidak ditentukan // (sebenarnya tidak ada Kesalahan JSON) }
Tentu saja, semuanya mungkin! Pemrogram memang membuat kesalahan. Bahkan dalam utilitas sumber terbuka yang digunakan oleh jutaan orang selama beberapa dekade – tiba-tiba ditemukan bug yang menyebabkan peretasan yang parah.
Dalam kasus kami, try...catch
ditempatkan untuk menangkap kesalahan "data yang salah". Namun berdasarkan sifatnya, catch
mendapatkan semua kesalahan dari try
. Di sini ia mendapat kesalahan yang tidak terduga, tetapi masih menampilkan pesan "JSON Error"
yang sama. Itu salah dan juga membuat kode lebih sulit untuk di-debug.
Untuk menghindari masalah seperti itu, kita dapat menggunakan teknik “melempar kembali”. Aturannya sederhana:
Catch seharusnya hanya memproses kesalahan yang diketahuinya dan “melempar kembali” semua kesalahan lainnya.
Teknik “rethrowing” dapat dijelaskan lebih detail sebagai berikut:
Catch mendapatkan semua kesalahan.
Di blok catch (err) {...}
kami menganalisis objek kesalahan err
.
Jika kami tidak tahu cara menanganinya, kami akan throw err
.
Biasanya, kita dapat memeriksa jenis kesalahan menggunakan operator instanceof
:
mencoba { pengguna = { /*...*/ }; } menangkap (salah) { if (err instanceofReferenceError) { alert('ReferenceError'); // "ReferenceError" untuk mengakses variabel yang tidak ditentukan } }
Kita juga bisa mendapatkan nama kelas error dari properti err.name
. Semua kesalahan asli memilikinya. Pilihan lainnya adalah membaca err.constructor.name
.
Pada kode di bawah ini, kita menggunakan rethrowing sehingga catch
hanya menangani SyntaxError
:
misalkan json = '{ "umur": 30 }'; // data tidak lengkap mencoba { biarkan pengguna = JSON.parse(json); if (!nama pengguna) { throw new SyntaxError("Data tidak lengkap: tidak ada nama"); } blabla(); // kesalahan tak terduga peringatan( nama pengguna ); } menangkap (salah) { jika (err instanceof SyntaxError) { alert("Kesalahan JSON: " + err.pesan ); } kalau tidak { membuang kesalahan; // melempar kembali (*) } }
Kesalahan yang muncul pada baris (*)
dari dalam blok catch
“jatuh” dari try...catch
dan dapat ditangkap oleh konstruksi try...catch
luar (jika ada), atau mematikan skrip.
Jadi blok catch
sebenarnya hanya menangani kesalahan yang diketahui cara menanganinya dan “melewatkan” kesalahan lainnya.
Contoh di bawah ini menunjukkan bagaimana kesalahan tersebut dapat ditangkap oleh satu level lagi try...catch
:
fungsi bacaData() { misalkan json = '{ "umur": 30 }'; mencoba { // ... blabla(); // kesalahan! } menangkap (salah) { // ... jika (!(err instanceof SyntaxError)) { membuang kesalahan; // rethrow (tidak tahu cara menghadapinya) } } } mencoba { bacaData(); } menangkap (salah) { alert("Tangkapan eksternal didapat: " + err ); // menangkapnya! }
Di sini readData
hanya mengetahui cara menangani SyntaxError
, sedangkan bagian luar try...catch
mengetahui cara menangani semuanya.
Tunggu, bukan itu saja.
Konstruksi try...catch
mungkin memiliki satu klausa kode lagi: finally
.
Jika ada, ini berjalan di semua kasus:
setelah try
, jika tidak ada kesalahan,
setelah catch
, jika ada kesalahan.
Sintaks yang diperluas terlihat seperti ini:
mencoba { ...coba jalankan kodenya... } menangkap (salah) { ... menangani kesalahan ... } Akhirnya { ... jalankan selalu ... }
Coba jalankan kode ini:
mencoba { peringatan('coba'); if (konfirmasi('Membuat kesalahan?')) BAD_CODE(); } menangkap (salah) { waspada('menangkap'); } Akhirnya { peringatan('akhirnya'); }
Kode ini memiliki dua cara eksekusi:
Jika Anda menjawab “Ya” untuk “Membuat kesalahan?”, lalu try -> catch -> finally
.
Jika Anda mengatakan "Tidak", try -> finally
.
Klausa finally
sering digunakan ketika kita mulai melakukan sesuatu dan ingin menyelesaikannya, apa pun hasilnya.
Misalnya, kita ingin mengukur waktu yang dibutuhkan oleh fungsi bilangan Fibonacci fib(n)
. Secara alami, kita dapat mulai mengukur sebelum dijalankan dan menyelesaikannya setelahnya. Namun bagaimana jika terjadi kesalahan saat pemanggilan fungsi? Secara khusus, penerapan fib(n)
pada kode di bawah ini mengembalikan kesalahan untuk bilangan negatif atau bukan bilangan bulat.
Klausa finally
adalah tempat yang bagus untuk menyelesaikan pengukuran apa pun yang terjadi.
Di sini finally
menjamin bahwa waktu akan diukur dengan benar dalam kedua situasi – jika eksekusi fib
berhasil dan jika terjadi kesalahan di dalamnya:
let num = +prompt("Masukkan bilangan bulat positif?", 35) biarkan berbeda, hasil; fungsi fib(n) { if (n < 0 || Matematika.trunc(n) != n) { throw new Error("Tidak boleh negatif, dan juga bilangan bulat."); } kembali n <= 1 ? n : fib(n - 1) + fib(n - 2); } biarkan mulai = Tanggal.sekarang(); mencoba { hasil = fib(angka); } menangkap (salah) { hasil = 0; } Akhirnya { diff = Tanggal.sekarang() - mulai; } alert(hasil || "terjadi kesalahan"); alert( `eksekusi memakan waktu ${diff}ms` );
Anda dapat memeriksanya dengan menjalankan kode dengan memasukkan 35
ke dalam prompt
– ini dijalankan secara normal, finally
setelah try
. Lalu masukkan -1
– akan ada kesalahan langsung, dan eksekusi akan memakan waktu 0ms
. Kedua pengukuran dilakukan dengan benar.
Dengan kata lain, fungsinya mungkin diakhiri dengan return
atau throw
, itu tidak masalah. Klausa finally
dijalankan dalam kedua kasus.
Variabel bersifat lokal di dalam try...catch...finally
Harap dicatat bahwa variabel result
dan diff
pada kode di atas dideklarasikan sebelum try...catch
.
Jika tidak, jika kita mendeklarasikan blok let
di try
, blok tersebut hanya akan terlihat di dalamnya.
finally
dan return
Klausa finally
berfungsi untuk setiap jalan keluar dari try...catch
. Itu termasuk return
yang eksplisit.
Pada contoh di bawah, ada return
di try
. Dalam hal ini, finally
dieksekusi tepat sebelum kontrol kembali ke kode luar.
fungsi fungsi() { mencoba { kembali 1; } menangkap (salah) { /* ... */ } Akhirnya { peringatan('akhirnya'); } } peringatan( fungsi() ); // peringatan pertama berfungsi dari akhirnya, lalu yang ini
try...finally
Konstruksi try...finally
, tanpa klausa catch
, juga berguna. Kami menerapkannya ketika kami tidak ingin menangani kesalahan di sini (membiarkannya gagal), namun ingin memastikan bahwa proses yang kami mulai telah diselesaikan.
fungsi fungsi() { // mulai melakukan sesuatu yang perlu diselesaikan (seperti pengukuran) mencoba { // ... } Akhirnya { // selesaikan hal itu meskipun semuanya mati } }
Pada kode di atas, error di dalam try
selalu muncul, karena tidak ada catch
. Namun finally
berfungsi sebelum alur eksekusi meninggalkan fungsinya.
Khusus untuk lingkungan
Informasi dari bagian ini bukan merupakan bagian dari inti JavaScript.
Bayangkan kita mendapat kesalahan fatal di luar try...catch
, dan skripnya mati. Seperti kesalahan pemrograman atau hal buruk lainnya.
Apakah ada cara untuk bereaksi terhadap kejadian seperti itu? Kita mungkin ingin mencatat kesalahannya, menunjukkan sesuatu kepada pengguna (biasanya mereka tidak melihat pesan kesalahan), dll.
Spesifikasinya tidak ada, tapi biasanya lingkungan menyediakannya, karena sangat berguna. Misalnya, Node.js memiliki process.on("uncaughtException")
untuk itu. Dan di browser kita dapat menetapkan fungsi ke properti window.onerror khusus, yang akan dijalankan jika terjadi kesalahan yang tidak tertangkap.
Sintaksnya:
window.onerror = function(pesan, url, baris, kolom, kesalahan) { // ... };
message
Pesan kesalahan.
url
URL skrip tempat terjadinya kesalahan.
line
, col
Nomor baris dan kolom tempat terjadinya kesalahan.
error
Objek kesalahan.
Misalnya:
<skrip> window.onerror = function(pesan, url, baris, kolom, kesalahan) { alert(`${message}n Pada ${line}:${col} dari ${url}`); }; fungsi bacaData() { fungsi buruk(); // Ups, ada yang tidak beres! } bacaData(); </skrip>
Peran pengendali global window.onerror
biasanya bukan untuk memulihkan eksekusi skrip – hal ini mungkin tidak mungkin dilakukan jika terjadi kesalahan pemrograman, namun untuk mengirimkan pesan kesalahan ke pengembang.
Ada juga layanan web yang menyediakan pencatatan kesalahan untuk kasus seperti ini, seperti https://errorception.com atau https://www.muscula.com.
Mereka bekerja seperti ini:
Kami mendaftar di layanan tersebut dan mendapatkan sepotong JS (atau URL skrip) dari mereka untuk disisipkan di halaman.
Skrip JS itu menetapkan fungsi window.onerror
khusus.
Ketika kesalahan terjadi, ia mengirimkan permintaan jaringan tentang hal itu ke layanan.
Kita dapat masuk ke antarmuka web layanan dan melihat kesalahan.
Konstruksi try...catch
memungkinkan untuk menangani kesalahan runtime. Ini secara harfiah memungkinkan untuk "mencoba" menjalankan kode dan "menangkap" kesalahan yang mungkin terjadi di dalamnya.
Sintaksnya adalah:
mencoba { // jalankan kode ini } menangkap (salah) { // jika terjadi kesalahan, lompat ke sini // err adalah objek kesalahan } Akhirnya { // lakukan apa pun setelah mencoba/menangkap }
Mungkin tidak ada bagian catch
atau tidak finally
, jadi konstruksi yang lebih pendek try...catch
dan try...finally
juga valid.
Objek kesalahan memiliki properti berikut:
message
– pesan kesalahan yang dapat dibaca manusia.
name
– string dengan nama kesalahan (nama konstruktor kesalahan).
stack
(tidak standar, tetapi didukung dengan baik) – tumpukan pada saat kesalahan dibuat.
Jika objek kesalahan tidak diperlukan, kita dapat menghilangkannya dengan menggunakan catch {
alih-alih catch (err) {
.
Kita juga dapat menghasilkan kesalahan kita sendiri menggunakan operator throw
. Secara teknis, argumen throw
bisa berupa apa saja, tapi biasanya berupa objek error yang diwarisi dari kelas Error
bawaan. Lebih lanjut tentang memperluas kesalahan di bab berikutnya.
Melempar ulang adalah pola penanganan kesalahan yang sangat penting: blok catch
biasanya mengharapkan dan mengetahui cara menangani jenis kesalahan tertentu, sehingga blok tersebut harus membuang kembali kesalahan yang tidak diketahuinya.
Bahkan jika kita tidak memiliki try...catch
, sebagian besar lingkungan memungkinkan kita menyiapkan penangan kesalahan "global" untuk menangkap kesalahan yang "jatuh". Di browser, itu window.onerror
.
pentingnya: 5
Bandingkan kedua potongan kode tersebut.
Yang pertama digunakan finally
untuk mengeksekusi kode setelah try...catch
:
mencoba { pekerjaan kerja } menangkap (salah) { menangani kesalahan } Akhirnya { membersihkan ruang kerja }
Fragmen kedua menempatkan pembersihan tepat setelah try...catch
:
mencoba { pekerjaan kerja } menangkap (salah) { menangani kesalahan } membersihkan ruang kerja
Pembersihan setelah pekerjaan pasti kita perlukan, tidak peduli ada kesalahan atau tidak.
Apakah ada keuntungan menggunakan finally
atau kedua fragmen kode sama? Jika ada keuntungan seperti itu, berikan contoh jika itu penting.
Perbedaannya menjadi jelas ketika kita melihat kode di dalam suatu fungsi.
Perilakunya berbeda jika ada "lompatan" dari try...catch
.
Misalnya, ketika ada return
di dalam try...catch
. Klausa finally
berfungsi jika ada jalan keluar dari try...catch
, bahkan melalui pernyataan return
: tepat setelah try...catch
selesai, tetapi sebelum kode panggilan mendapatkan kendali.
fungsi f() { mencoba { peringatan('mulai'); kembalikan "hasil"; } menangkap (salah) { /// ... } Akhirnya { waspada('bersihkan!'); } } F(); // pembersihan!
…Atau saat ada throw
, seperti di sini:
fungsi f() { mencoba { peringatan('mulai'); melempar Kesalahan baru("kesalahan"); } menangkap (salah) { // ... if("tidak dapat menangani kesalahan") { membuang kesalahan; } } Akhirnya { waspada('bersihkan!') } } F(); // pembersihan!
finally
yang menjamin pembersihan di sini. Jika kita hanya meletakkan kode di akhir f
, kode tersebut tidak akan berjalan dalam situasi ini.