Bab 8 dari Eloquent JavaScript. Sebelumnya: 07 Proyek Robot Pengantar Paket. Berikutnya: 09 Regular Expression.

Debugging itu dua kali lebih susah daripada nulis kodenya di awal. Makanya, kalau kamu nulis kode secerdik mungkin, kamu, berdasarkan definisi, gak cukup pintar buat men-debug-nya.

— Brian Kernighan dan P.J. Plauger, The Elements of Programming Style

Ilustrasi berbagai serangga dan seekor kelabang

Cacat di program komputer biasanya disebut bug. Programmer merasa senang ngebayangin mereka sebagai makhluk kecil yang kebetulan merayap masuk ke kerjaan kita. Kenyataannya, tentu aja, kita sendiri yang naruhnya di sana.

Kalau program itu pikiran yang dikristalkan, kita bisa secara kasar ngategoriin bug jadi yang disebabkan pikiran yang bingung dan yang disebabkan kesalahan yang muncul waktu ngonversi pikiran jadi kode. Tipe pertama umumnya lebih susah didiagnosis dan diperbaiki daripada yang kedua.

Bahasa

Banyak kesalahan bisa ditunjukin ke kita secara otomatis sama komputer kalau dia cukup tahu soal apa yang kita coba lakuin. Tapi di sini, kelonggaran JavaScript jadi penghalang. Konsep binding dan propertinya cukup samar sampai dia jarang nangkep typo sebelum beneran ngejalanin programnya. Bahkan waktu itu pun, dia ngebolehin kamu ngelakuin beberapa hal yang jelas gak masuk akal tanpa protes, kaya ngitung true * "monkey".

Ada beberapa hal yang JavaScript emang protes. Nulis program yang gak ngikutin tata bahasa bahasanya bakal langsung bikin komputer protes. Hal lain, kaya manggil sesuatu yang bukan fungsi atau nyari properti di nilai undefined, bakal bikin error dilaporin waktu programnya nyoba ngelakuin aksinya.

Tapi sering kali, perhitungan gak masuk akalmu cuma bakal ngehasilin NaN (not a number) atau nilai undefined, sementara programnya dengan senang hati lanjut, yakin dia ngelakuin sesuatu yang bermakna. Kesalahannya baru bakal muncul belakangan, setelah nilai palsunya udah jalan-jalan lewat beberapa fungsi. Dia mungkin gak memicu error sama sekali, tapi diam-diam bikin output programnya salah. Nemuin sumber masalah kaya gitu bisa susah.

Proses nemuin kesalahan, bug, di program disebut debugging.

Strict Mode

JavaScript bisa dibikin sedikit lebih ketat dengan ngaktifin strict mode. Ini bisa dilakuin dengan naruh string "use strict" di atas file atau badan fungsi. Ini contohnya:

function canYouSpotTheProblem() {
  "use strict";
  for (counter = 0; counter < 10; counter++) {
    console.log("Happy happy");
  }
}
 
canYouSpotTheProblem();
// → ReferenceError: counter is not defined

Kode di dalam class dan modul (yang bakal kita bahas di Bab 10) otomatis strict. Perilaku lama yang gak strict masih ada cuma karena beberapa kode lama mungkin bergantung padanya, dan perancang bahasanya kerja keras buat gak ngerusak program yang udah ada.

Biasanya, waktu kamu lupa naruh let di depan bindingmu, kaya counter di contoh, JavaScript diam-diam bikin binding global dan pakai itu. Di strict mode, error dilaporin sebagai gantinya. Ini sangat membantu. Tapi perlu dicatat, ini gak jalan waktu binding yang dimaksud udah ada di suatu tempat di scope. Di kasus itu, loop-nya masih bakal diam-diam nimpa nilai bindingnya.

Perubahan lain di strict mode adalah binding this megang nilai undefined di fungsi yang gak dipanggil sebagai method. Waktu bikin pemanggilan kaya gitu di luar strict mode, this ngerujuk ke objek scope global, yaitu objek yang propertinya adalah binding global. Jadi kalau kamu gak sengaja manggil method atau constructor dengan salah di strict mode, JavaScript bakal ngehasilin error begitu dia nyoba baca sesuatu dari this, bukannya dengan senang hati nulis ke scope global.

Misalnya, lihat kode berikut, yang manggil fungsi constructor tanpa keyword new jadi this-nya gak bakal ngerujuk ke objek yang baru dibangun:

function Person(name) { this.name = name; }
let ferdinand = Person("Ferdinand"); // ups
console.log(name);
// → Ferdinand

Pemanggilan palsu ke Person berhasil, tapi ngembaliin nilai undefined dan bikin binding global name. Di strict mode, hasilnya beda.

"use strict";
function Person(name) { this.name = name; }
let ferdinand = Person("Ferdinand"); // lupa new
// → TypeError: Cannot set property 'name' of undefined

Kita langsung dikasih tahu ada yang salah. Ini membantu.

Untungnya, constructor yang dibikin dengan notasi class bakal selalu protes kalau dipanggil tanpa new, bikin ini kurang jadi masalah bahkan di mode gak strict.

Strict mode ngelakuin beberapa hal lagi. Dia ngelarang ngasih fungsi beberapa parameter dengan nama yang sama dan ngehapus fitur bahasa yang bermasalah sepenuhnya (kaya statement with, yang begitu salahnya sampai gak dibahas lebih lanjut di buku ini).

Singkatnya, naruh "use strict" di atas programmu jarang merugikan dan mungkin ngebantu kamu nemuin masalah.

Tipe

Beberapa bahasa mau tahu tipe dari semua binding dan ekspresimu bahkan sebelum ngejalanin program. Mereka bakal langsung ngasih tahu kamu waktu sebuah tipe dipakai dengan cara yang gak konsisten. JavaScript cuma mempertimbangkan tipe waktu beneran ngejalanin programnya, dan bahkan di sana pun sering nyoba secara implisit ngonversi nilai ke tipe yang dia harapkan, jadi gak banyak membantu.

Tetap aja, tipe nyediain kerangka yang berguna buat ngomongin program. Banyak kesalahan datang dari kebingungan soal jenis nilai yang masuk ke atau keluar dari sebuah fungsi. Kalau kamu punya informasi itu tertulis, kamu lebih kecil kemungkinannya bingung.

Kamu bisa nambahin komentar kaya berikut sebelum fungsi findRoute dari bab sebelumnya buat menggambarkan tipenya:

// (graph: Object, from: string, to: string) => string[]
function findRoute(graph, from, to) {
  // ...
}

Ada sejumlah konvensi berbeda buat nganotasi program JavaScript dengan tipe.

Satu hal soal tipe adalah mereka perlu ngenalin kerumitan mereka sendiri biar bisa menggambarkan cukup banyak kode buat berguna. Menurutmu apa tipe dari fungsi randomPick yang ngembaliin elemen acak dari array? Kamu perlu ngenalin variabel tipe, T, yang bisa mewakili tipe apa pun, biar kamu bisa ngasih randomPick tipe kaya (T[]) → T (fungsi dari array T ke sebuah T).

Waktu tipe-tipe sebuah program diketahui, komputer bisa ngecek-nya buat kamu, nunjukin kesalahan sebelum programnya dijalanin. Ada beberapa dialek JavaScript yang nambahin tipe ke bahasanya dan ngeceknya. Yang paling populer disebut TypeScript. Kalau kamu tertarik nambahin ketelitian ke programmu, saya nyaranin kamu nyobanya.

Di buku ini, kita bakal terus pakai kode JavaScript mentah, berbahaya, dan tanpa tipe.

Testing

Kalau bahasanya gak bakal banyak ngebantu kita nemuin kesalahan, kita harus nemuinnya dengan cara susah: ngejalanin programnya dan lihat apakah dia ngelakuin hal yang benar.

Ngelakuin ini secara manual, lagi dan lagi, adalah ide yang sangat buruk. Bukan cuma ngeselin tapi juga cenderung gak efektif, karena butuh terlalu banyak waktu buat ngetes semuanya secara menyeluruh setiap kali kamu bikin perubahan.

Komputer jago di tugas berulang, dan testing adalah tugas berulang yang ideal. Automated testing (pengujian otomatis) adalah proses nulis program yang ngetes program lain. Nulis tes sedikit lebih banyak kerjaan daripada ngetes manual, tapi begitu kamu udah ngelakuinnya, kamu dapet semacam kekuatan super: cuma butuh beberapa detik buat memverifikasi bahwa programmu masih berperilaku benar di semua situasi yang kamu tulis tesnya. Waktu kamu ngerusak sesuatu, kamu bakal langsung nyadar bukannya secara acak nemuinnya di waktu yang lebih belakangan.

Tes biasanya berbentuk program kecil berlabel yang memverifikasi suatu aspek dari kodemu. Misalnya, sekumpulan tes buat method toUpperCase (yang standar, mungkin udah dites orang lain) mungkin keliatan kaya gini:

function test(label, body) {
  if (!body()) console.log(`Failed: ${label}`);
}
 
test("convert Latin text to uppercase", () => {
  return "hello".toUpperCase() == "HELLO";
});
test("convert Greek text to uppercase", () => {
  return "Χαίρετε".toUpperCase() == "ΧΑΊΡΕΤΕ";
});
test("don't convert case-less characters", () => {
  return "مرحبا".toUpperCase() == "مرحبا";
});

Nulis tes kaya gini cenderung ngehasilin kode yang agak berulang dan canggung. Untungnya, ada software yang ngebantu kamu ngebangun dan ngejalanin kumpulan tes (test suite) dengan nyediain bahasa (dalam bentuk fungsi dan method) yang cocok buat ngekspresiin tes dan dengan ngeluarin informasi yang informatif waktu tes gagal. Ini biasanya disebut test runner.

Beberapa kode lebih gampang dites dari kode lain. Umumnya, makin banyak objek eksternal yang berinteraksi sama kodenya, makin susah nyiapin konteks buat ngetesnya. Gaya pemrograman yang ditunjukin di bab sebelumnya, yang pakai nilai persistent yang berdiri sendiri bukannya objek yang berubah, cenderung gampang dites.

Debugging

Begitu kamu nyadar ada yang salah sama programmu karena dia berperilaku salah atau ngehasilin error, langkah berikutnya adalah nyari tahu apa masalahnya.

Kadang jelas. Pesan error-nya bakal nunjuk ke baris tertentu di programmu, dan kalau kamu ngelihat deskripsi error-nya dan baris kode itu, kamu sering bisa ngelihat masalahnya.

Tapi gak selalu. Kadang baris yang memicu masalahnya cuma tempat pertama di mana nilai gak stabil yang dihasilin di tempat lain dipakai dengan cara yang gak valid. Kalau kamu udah ngerjain latihan di bab-bab sebelumnya, kamu mungkin udah ngalamin situasi kaya gitu.

Program contoh berikut nyoba ngonversi bilangan bulat jadi string di basis tertentu (desimal, biner, dan seterusnya) dengan berulang kali ngambil digit terakhir lalu ngebagi angkanya buat ngilangin digit ini. Tapi output aneh yang dia hasilkan sekarang nunjukin dia punya bug.

function numberToString(n, base = 10) {
  let result = "", sign = "";
  if (n < 0) {
    sign = "-";
    n = -n;
  }
  do {
    result = String(n % base) + result;
    n /= base;
  } while (n > 0);
  return sign + result;
}
console.log(numberToString(13, 10));
// → 1.5e-3231.3e-3221.3e-3211.3e-3201.3e-3191.3e-3181.3…

Bahkan kalau kamu udah ngelihat masalahnya, pura-pura sebentar kamu gak. Kita tahu program kita gak berfungsi dengan benar, dan kita mau nyari tahu kenapa.

Di sinilah kamu harus nahan dorongan buat mulai bikin perubahan acak ke kode buat lihat apakah itu bikinnya lebih baik. Sebagai gantinya, berpikir. Analisis apa yang terjadi dan buat teori kenapa itu mungkin terjadi. Lalu bikin pengamatan tambahan buat ngetes teori ini, atau, kalau kamu belum punya teori, bikin pengamatan tambahan buat ngebantu kamu nemuin satu.

Naruh beberapa pemanggilan console.log yang strategis ke dalam program adalah cara bagus buat dapet informasi tambahan soal apa yang dilakuin programnya. Di kasus ini, kita mau n ngambil nilai 13, 1, lalu 0. Kita tulis nilainya di awal loop.

13
1.3
0.13
0.013
…
1.5e-323

Nah. Ngebagi 13 dengan 10 gak ngehasilin bilangan bulat. Bukannya n /= base, yang sebenernya kita mau adalah n = Math.floor(n / base) biar angkanya “digeser” dengan benar ke kanan.

Alternatif dari pakai console.log buat ngintip perilaku program adalah pakai kemampuan debugger dari browsermu. Browser datang dengan kemampuan buat nyetel breakpoint di baris tertentu dari kodemu. Waktu eksekusi program nyampe baris dengan breakpoint, dia dijeda, dan kamu bisa meriksa nilai binding di titik itu. Saya gak bakal masuk ke detail, karena debugger beda-beda antar browser, tapi lihat di developer tools browsermu atau cari di web buat instruksinya.

Cara lain buat nyetel breakpoint adalah nyertain statement debugger (yang cuma terdiri dari keyword itu) di programmu. Kalau developer tools browsermu aktif, programnya bakal dijeda setiap kali nyampe statement kaya gitu.

Penyebaran Error

Gak semua masalah bisa dicegah programmer, sayangnya. Kalau programmu berkomunikasi sama dunia luar dengan cara apa pun, bisa aja dapet input yang rusak, jadi kelebihan beban kerja, atau jaringannya gagal.

Kalau kamu memprogram cuma buat dirimu sendiri, kamu bisa cuma ngabaikan masalah kaya gitu sampai mereka terjadi. Tapi kalau kamu ngebangun sesuatu yang bakal dipakai orang lain, kamu biasanya mau programnya lebih baik dari sekadar crash. Kadang hal yang benar buat dilakuin adalah nerima input buruknya dengan tenang dan terus jalan. Di kasus lain, lebih baik ngelaporin ke pengguna apa yang salah lalu menyerah. Di kedua situasi programnya harus aktif ngelakuin sesuatu sebagai respons terhadap masalahnya.

Misalnya kamu punya fungsi promptNumber yang minta pengguna masukin angka dan ngembaliinnya. Apa yang harus dia kembaliin kalau penggunanya masukin “orange”?

Salah satu pilihannya adalah bikin dia ngembaliin nilai spesial. Pilihan umum buat nilai kaya gitu adalah null, undefined, atau -1.

function promptNumber(question) {
  let result = Number(prompt(question));
  if (Number.isNaN(result)) return null;
  else return result;
}
 
console.log(promptNumber("How many trees do you see?"));

Sekarang kode apa pun yang manggil promptNumber harus ngecek apakah angka beneran yang dibaca dan, kalau gak, harus entah gimana pulih, mungkin dengan nanya lagi atau ngisi nilai default. Atau dia bisa lagi-lagi ngembaliin nilai spesial ke pemanggilnya buat nunjukin dia gagal ngelakuin apa yang diminta.

Di banyak situasi, terutama waktu error itu umum dan pemanggilnya harus secara eksplisit memperhitungkannya, ngembaliin nilai spesial adalah cara bagus buat nunjukin error. Tapi dia punya sisi buruknya. Pertama, gimana kalau fungsinya udah bisa ngembaliin setiap jenis nilai yang mungkin? Di fungsi kaya gitu, kamu harus ngelakuin sesuatu kaya ngebungkus hasilnya di objek biar bisa ngebedain keberhasilan dari kegagalan, kaya yang dilakuin method next di interface iterator.

function lastElement(array) {
  if (array.length == 0) {
    return {failed: true};
  } else {
    return {value: array[array.length - 1]};
  }
}

Masalah kedua dengan ngembaliin nilai spesial adalah bisa ngarah ke kode yang canggung. Kalau sepotong kode manggil promptNumber 10 kali, dia harus ngecek 10 kali apakah null dikembaliin. Kalau responsnya waktu nemuin null adalah cuma ngembaliin null sendiri, pemanggil fungsinya pada gilirannya harus ngeceknya, dan seterusnya.

Exception

Waktu fungsi gak bisa lanjut secara normal, yang sering kita mau lakuin adalah cuma berhenti ngelakuin apa yang kita lakuin dan langsung lompat ke tempat yang tahu cara nanganin masalahnya. Inilah yang dilakuin exception handling.

Exception adalah mekanisme yang bikin mungkin buat kode yang nemuin masalah ngangkat (raise) (atau ngelempar, throw) sebuah exception. Exception bisa nilai apa pun. Ngangkatnya agak mirip return dari fungsi yang dipompa: dia lompat keluar bukan cuma dari fungsi saat ini tapi juga pemanggilnya, sampai ke bawah ke pemanggilan pertama yang memulai eksekusi saat ini. Ini disebut unwinding the stack (ngegulung stack). Kamu mungkin ingat stack pemanggilan fungsi yang disebutin di Bab 3. Exception meluncur turun di stack ini, ngebuang semua konteks pemanggilan yang dia temuin.

Kalau exception selalu langsung meluncur ke dasar stack, mereka gak bakal banyak gunanya. Mereka cuma bakal nyediain cara baru buat ngeledakin programmu. Kekuatan mereka ada di fakta bahwa kamu bisa nyetel “rintangan” sepanjang stack buat nangkep (catch) exception-nya waktu dia meluncur turun. Begitu kamu udah nangkep exception, kamu bisa ngelakuin sesuatu dengannya buat ngatasin masalahnya lalu terus ngejalanin programnya.

Ini contohnya:

function promptDirection(question) {
  let result = prompt(question);
  if (result.toLowerCase() == "left") return "L";
  if (result.toLowerCase() == "right") return "R";
  throw new Error("Invalid direction: " + result);
}
 
function look() {
  if (promptDirection("Which way?") == "L") {
    return "a house";
  } else {
    return "two angry bears";
  }
}
 
try {
  console.log("You see", look());
} catch (error) {
  console.log("Something went wrong: " + error);
}

Keyword throw dipakai buat ngangkat exception. Nangkepnya dilakuin dengan ngebungkus sepotong kode di block try, diikuti keyword catch. Waktu kode di block try bikin exception diangkat, block catch dievaluasi, dengan nama di dalam tanda kurung terikat ke nilai exception-nya. Setelah block catch selesai, atau kalau block try selesai tanpa masalah, programnya lanjut di bawah seluruh statement try/catch.

Di kasus ini, kita pakai constructor Error buat bikin nilai exception kita. Ini constructor JavaScript standar yang bikin objek dengan properti message. Instance Error juga ngumpulin informasi soal call stack yang ada waktu exception-nya dibikin, yang disebut stack trace. Informasi ini disimpan di properti stack dan bisa berguna waktu nyoba men-debug masalah: dia ngasih tahu kita fungsi tempat masalahnya terjadi dan fungsi mana yang bikin pemanggilan yang gagal.

Perhatiin bahwa fungsi look sepenuhnya ngabaikan kemungkinan bahwa promptDirection mungkin salah. Ini keuntungan besar dari exception: kode penanganan error cuma perlu di titik tempat error-nya terjadi dan di titik tempat dia ditangani. Fungsi-fungsi di antaranya bisa lupa soal itu sepenuhnya.

Yah, hampir…

Beres-beres Setelah Exception

Efek exception adalah jenis alur kontrol lain. Setiap aksi yang mungkin bikin exception, yang kurang lebih setiap pemanggilan fungsi dan akses properti, mungkin bikin kontrol tiba-tiba ninggalin kodemu.

Ini berarti waktu kode punya beberapa side effect, bahkan kalau alur kontrol “biasa”-nya keliatan kaya semuanya bakal selalu terjadi, exception mungkin nyegah beberapa di antaranya terjadi.

Ini kode perbankan yang sangat buruk:

const accounts = {
  a: 100,
  b: 0,
  c: 20
};
 
function getAccount() {
  let accountName = prompt("Enter an account name");
  if (!Object.hasOwn(accounts, accountName)) {
    throw new Error(`No such account: ${accountName}`);
  }
  return accountName;
}
 
function transfer(from, amount) {
  if (accounts[from] < amount) return;
  accounts[from] -= amount;
  accounts[getAccount()] += amount;
}

Fungsi transfer mentransfer sejumlah uang dari akun tertentu ke akun lain, nanya nama akun lainnya dalam prosesnya. Kalau dikasih nama akun yang gak valid, getAccount ngelempar exception.

Tapi transfer pertama ngehapus uangnya dari akun dan lalu manggil getAccount sebelum nambahinnya ke akun lain. Kalau dia diputus sama exception di titik itu, dia cuma bakal bikin uangnya hilang.

Kode itu bisa ditulis sedikit lebih cerdas, misalnya dengan manggil getAccount sebelum mulai mindahin uang. Tapi sering masalah kaya gini terjadi dengan cara yang lebih halus. Bahkan fungsi yang gak keliatan bakal ngelempar exception mungkin ngelakuinnya di keadaan luar biasa atau waktu mereka berisi kesalahan programmer.

Salah satu cara ngatasin ini adalah pakai lebih sedikit side effect. Lagi-lagi, gaya pemrograman yang ngitung nilai baru bukannya ngubah data yang ada itu membantu. Kalau sepotong kode berhenti jalan di tengah bikin nilai baru, gak ada struktur data yang ada yang rusak, bikin lebih gampang pulih.

Karena itu gak selalu praktis, statement try punya fitur lain: mereka bisa diikuti block finally baik sebagai pengganti atau tambahan dari block catch. Block finally bilang “gak peduli apa pun yang terjadi, jalanin kode ini setelah nyoba ngejalanin kode di block try.”

function transfer(from, amount) {
  if (accounts[from] < amount) return;
  let progress = 0;
  try {
    accounts[from] -= amount;
    progress = 1;
    accounts[getAccount()] += amount;
    progress = 2;
  } finally {
    if (progress == 1) {
      accounts[from] += amount;
    }
  }
}

Versi fungsi ini ngelacak kemajuannya, dan kalau, waktu keluar, dia nyadar dia diputus di titik di mana dia udah bikin keadaan program yang gak konsisten, dia memperbaiki kerusakan yang dia bikin.

Perhatiin bahwa walaupun kode finally dijalanin waktu exception dilempar di block try, dia gak ngeganggu exception-nya. Setelah block finally jalan, stack-nya terus digulung.

Nulis program yang beroperasi dengan andal bahkan waktu exception muncul di tempat yang gak terduga itu susah. Banyak orang cuma gak repot-repot, dan karena exception biasanya dicadangkan buat keadaan luar biasa, masalahnya mungkin terjadi begitu jarang sampai gak pernah disadari. Apakah itu hal bagus atau hal yang sangat buruk tergantung seberapa besar kerusakan yang bakal dibikin softwarenya waktu gagal.

Nangkep Secara Selektif

Waktu exception nyampe sampai ke dasar stack tanpa ditangkep, dia ditangani sama environment-nya. Apa artinya ini beda antar environment. Di browser, deskripsi error-nya biasanya ditulis ke konsol JavaScript (bisa diakses lewat menu Tools atau Developer browser). Node.js, environment JavaScript tanpa browser yang bakal kita bahas di Bab 20, lebih hati-hati soal korupsi data. Dia ngehentiin seluruh proses waktu exception yang gak ditangani terjadi.

Buat kesalahan programmer, cuma ngebiarin error-nya lewat sering kali yang terbaik yang bisa kamu lakuin. Exception yang gak ditangani adalah cara yang masuk akal buat ngasih sinyal program yang rusak, dan konsol JavaScript, di browser modern, bakal ngasih kamu beberapa informasi soal pemanggilan fungsi mana yang ada di stack waktu masalahnya terjadi.

Buat masalah yang diharapkan terjadi selama penggunaan rutin, crash dengan exception yang gak ditangani adalah strategi yang mengerikan.

Penggunaan bahasa yang gak valid, kaya ngerujuk binding yang gak ada, nyari properti di null, atau manggil sesuatu yang bukan fungsi, juga bakal bikin exception diangkat. Exception kaya gitu juga bisa ditangkep.

Waktu badan catch dimasuki, yang kita tahu cuma sesuatu di badan try kita bikin exception. Tapi kita gak tahu apa yang bikin atau exception mana yang dia bikin.

JavaScript (dalam kelalaian yang cukup mencolok) gak nyediain dukungan langsung buat nangkep exception secara selektif: entah kamu nangkep semuanya atau gak nangkep sama sekali. Ini bikin menggoda buat nganggap bahwa exception yang kamu dapet adalah yang kamu pikirin waktu nulis block catch.

Tapi mungkin bukan. Asumsi lain mungkin dilanggar, atau kamu mungkin udah ngenalin bug yang bikin exception. Ini contoh yang nyoba terus manggil promptDirection sampai dapet jawaban valid:

for (;;) {
  try {
    let dir = promtDirection("Where?"); // ← typo!
    console.log("You chose ", dir);
    break;
  } catch (e) {
    console.log("Not a valid direction. Try again.");
  }
}

Konstruksi for (;;) adalah cara buat sengaja bikin loop yang gak berhenti sendiri. Kita keluar dari loop cuma waktu arah yang valid dikasih. Sayangnya, kita salah ngeja promptDirection, yang bakal ngehasilin error “undefined variable”. Karena block catch sepenuhnya ngabaikan nilai exception-nya (e), nganggap dia tahu apa masalahnya, dia salah memperlakukan error binding sebagai tanda input buruk. Bukan cuma ini bikin infinite loop tapi juga “ngubur” pesan error yang berguna soal binding yang salah eja.

Sebagai aturan umum, jangan nangkep exception secara membabi buta kecuali buat tujuan “ngerutekan” mereka ke suatu tempat, misalnya, lewat jaringan buat ngasih tahu sistem lain bahwa program kita crash. Dan bahkan waktu itu pun, pikirkan baik-baik gimana kamu mungkin nyembunyiin informasi.

Kita mau nangkep jenis exception yang spesifik. Kita bisa ngelakuin ini dengan ngecek di block catch apakah exception yang kita dapet adalah yang kita minati, dan kalau gak, ngelempar ulang. Tapi gimana kita ngenalin exception?

Kita bisa ngebandingin properti message-nya dengan pesan error yang kebetulan kita harapkan. Tapi itu cara yang goyah buat nulis kode, kita bakal pakai informasi yang dimaksudkan buat konsumsi manusia (pesannya) buat bikin keputusan programatik. Begitu seseorang ngubah (atau nerjemahin) pesannya, kodenya bakal berhenti jalan.

Sebagai gantinya, kita definisikan tipe error baru dan pakai instanceof buat ngenalinnya.

class InputError extends Error {}
 
function promptDirection(question) {
  let result = prompt(question);
  if (result.toLowerCase() == "left") return "L";
  if (result.toLowerCase() == "right") return "R";
  throw new InputError("Invalid direction: " + result);
}

Class error barunya extends Error. Dia gak mendefinisikan constructor-nya sendiri, yang berarti dia mewarisi constructor Error, yang ngarepin string pesan sebagai argumen. Sebenernya, dia gak mendefinisikan apa pun sama sekali, class-nya kosong. Objek InputError berperilaku kaya objek Error, kecuali mereka punya class berbeda yang bisa kita pakai buat ngenalinnya.

Sekarang loop-nya bisa nangkep ini dengan lebih hati-hati.

for (;;) {
  try {
    let dir = promptDirection("Where?");
    console.log("You chose ", dir);
    break;
  } catch (e) {
    if (e instanceof InputError) {
      console.log("Not a valid direction. Try again.");
    } else {
      throw e;
    }
  }
}

Ini cuma bakal nangkep instance InputError dan ngebiarin exception yang gak terkait lewat. Kalau kamu masukin lagi typo-nya, error binding undefined-nya bakal dilaporin dengan benar.

Assertion

Assertion adalah pengecekan di dalam program yang memverifikasi bahwa sesuatu sesuai dengan yang seharusnya. Mereka dipakai bukan buat nanganin situasi yang bisa muncul di operasi normal tapi buat nemuin kesalahan programmer.

Kalau, misalnya, firstElement digambarkan sebagai fungsi yang gak boleh dipanggil dengan array kosong, kita mungkin nulisnya kaya gini:

function firstElement(array) {
  if (array.length == 0) {
    throw new Error("firstElement called with []");
  }
  return array[0];
}

Sekarang, bukannya diam-diam ngembaliin undefined (yang kamu dapet waktu baca properti array yang gak ada), ini bakal dengan keras ngeledakin programmu begitu kamu salah pakainya. Ini bikin kesalahan kaya gitu lebih kecil kemungkinannya luput dan lebih gampang nemuin penyebabnya waktu terjadi.

Saya gak nyaranin nyoba nulis assertion buat setiap jenis input buruk yang mungkin. Itu bakal banyak kerjaan dan ngarah ke kode yang sangat berisik. Kamu bakal mau nyimpennya buat kesalahan yang gampang dibikin (atau yang kamu dapati dirimu bikin).

Ringkasan

Bagian penting dari pemrograman adalah nemuin, mendiagnosis, dan memperbaiki bug. Masalah bisa jadi lebih gampang disadari kalau kamu punya test suite otomatis atau nambahin assertion ke programmu.

Masalah yang disebabkan faktor di luar kendali program biasanya harus aktif direncanain. Kadang, waktu masalahnya bisa ditangani secara lokal, nilai kembalian spesial adalah cara bagus buat ngelacaknya. Kalau gak, exception mungkin lebih disukai.

Ngelempar exception bikin call stack digulung sampai block try/catch terdekat yang ngelingkupinya atau sampai dasar stack. Nilai exception-nya bakal dikasih ke block catch yang nangkepnya, yang harus memverifikasi bahwa dia beneran jenis exception yang diharapkan lalu ngelakuin sesuatu dengannya. Buat ngebantu ngatasin alur kontrol yang gak terduga yang disebabkan exception, block finally bisa dipakai buat mastiin sepotong kode selalu jalan waktu sebuah block selesai.

Poin-poin pentingnya:

  1. "use strict" bikin JavaScript lebih ketat: lupa let jadi error, this di fungsi biasa jadi undefined. Hampir gak ada ruginya.
  2. Tulis tipe fungsi di komentar biar gak bingung; kalau mau dicek otomatis, coba TypeScript.
  3. Tes otomatis = program yang ngetes program. Kode dengan sedikit side effect lebih gampang dites.
  4. Waktu debugging: berpikir, bikin teori, lalu amati pakai console.log atau breakpoint/debugger. Jangan asal ubah kode.
  5. Dua cara nyebarin error: ngembaliin nilai spesial (null, {failed: true}) atau ngelempar exception (throw).
  6. try/catch nangkep exception, finally selalu jalan apa pun yang terjadi (buat beres-beres).
  7. Jangan nangkep semua exception secara buta. Bikin class error sendiri (class InputError extends Error {}), cek pakai instanceof, dan throw e ulang kalau bukan yang kamu tunggu.
  8. Assertion = throw waktu asumsi programmer dilanggar, biar bug cepat ketahuan.

Latihan

Retry

Misalnya kamu punya fungsi primitiveMultiply yang di 20 persen kasus ngaliin dua angka dan di 80 persen kasus lainnya ngangkat exception bertipe MultiplicatorUnitFailure. Tulis fungsi yang ngebungkus fungsi kikuk ini dan cuma terus nyoba sampai sebuah pemanggilan berhasil, setelah itu dia ngembaliin hasilnya.

Pastiin kamu cuma nanganin exception yang kamu coba tangani.

class MultiplicatorUnitFailure extends Error {}
 
function primitiveMultiply(a, b) {
  if (Math.random() < 0.2) {
    return a * b;
  } else {
    throw new MultiplicatorUnitFailure("Klunk");
  }
}
 
function reliableMultiply(a, b) {
  // Kodemu di sini.
}
 
console.log(reliableMultiply(8, 8));
// → 64

Petunjuk:

Pemanggilan primitiveMultiply jelas harus terjadi di block try. Block catch yang sesuai harus ngelempar ulang exception-nya waktu dia bukan instance MultiplicatorUnitFailure dan mastiin pemanggilannya dicoba lagi waktu iya.

Buat ngelakuin percobaan ulangnya, kamu bisa pakai loop yang cuma berhenti waktu pemanggilan berhasil, kaya di contoh `promptDirection` sebelumnya di bab ini, atau pakai rekursi dan berharap kamu gak dapet rangkaian kegagalan yang begitu panjang sampai stack-nya overflow (yang taruhannya cukup aman).

Solusi:

class MultiplicatorUnitFailure extends Error {}
 
function primitiveMultiply(a, b) {
  if (Math.random() < 0.2) {
    return a * b;
  } else {
    throw new MultiplicatorUnitFailure("Klunk");
  }
}
 
function reliableMultiply(a, b) {
  for (;;) {
    try {
      return primitiveMultiply(a, b);
    } catch (e) {
      if (!(e instanceof MultiplicatorUnitFailure))
        throw e;
    }
  }
}
 
console.log(reliableMultiply(8, 8));
// → 64

Kotak Terkunci

Lihat objek berikut (yang agak dibuat-buat):

const box = new class {
  locked = true;
  #content = [];
 
  unlock() { this.locked = false; }
  lock() { this.locked = true;  }
  get content() {
    if (this.locked) throw new Error("Locked!");
    return this.#content;
  }
};

Ini kotak dengan kunci. Ada array di dalam kotaknya, tapi kamu cuma bisa ngambilnya waktu kotaknya gak terkunci.

Tulis fungsi bernama withBoxUnlocked yang ngambil nilai fungsi sebagai argumen, ngebuka kunci kotaknya, ngejalanin fungsinya, lalu mastiin kotaknya dikunci lagi sebelum return, terlepas dari apakah fungsi argumennya return secara normal atau ngelempar exception.

const box = new class {
  locked = true;
  #content = [];
 
  unlock() { this.locked = false; }
  lock() { this.locked = true;  }
  get content() {
    if (this.locked) throw new Error("Locked!");
    return this.#content;
  }
};
 
function withBoxUnlocked(body) {
  // Kodemu di sini.
}
 
withBoxUnlocked(() => {
  box.content.push("gold piece");
});
 
try {
  withBoxUnlocked(() => {
    throw new Error("Pirates on the horizon! Abort!");
  });
} catch (e) {
  console.log("Error raised: " + e);
}
console.log(box.locked);
// → true

Buat poin ekstra, pastiin kalau kamu manggil withBoxUnlocked waktu kotaknya udah gak terkunci, kotaknya tetap gak terkunci.

Petunjuk:

Latihan ini butuh block finally. Fungsimu harus pertama ngebuka kunci kotaknya lalu manggil fungsi argumennya dari dalam badan try. Block finally setelahnya harus ngunci kotaknya lagi.

Buat mastiin kita gak ngunci kotaknya waktu dia sebelumnya gak terkunci, cek kuncinya di awal fungsi dan buka dan kunci cuma waktu dia awalnya terkunci.

Solusi:

const box = new class {
  locked = true;
  #content = [];
 
  unlock() { this.locked = false; }
  lock() { this.locked = true;  }
  get content() {
    if (this.locked) throw new Error("Locked!");
    return this.#content;
  }
};
 
function withBoxUnlocked(body) {
  let locked = box.locked;
  if (locked) box.unlock();
  try {
    return body();
  } finally {
    if (locked) box.lock();
  }
}
 
withBoxUnlocked(() => {
  box.content.push("gold piece");
});
 
try {
  withBoxUnlocked(() => {
    throw new Error("Pirates on the horizon! Abort!");
  });
} catch (e) {
  console.log("Error raised:", e);
}
 
console.log(box.locked);
// → true