Setiap pemuatan PDF mengalami crash dengan ReferenceError yang sama. Stack trace-nya tidak menunjukkan petunjuk yang berguna, dan spesifikasi yang memandu kode tersebut tampak sangat masuk akal di atas kertas. Spesifikasi itu menyuruh untuk memeriksa feature flag, dan membandingkan setiap region dengan setengah dari pageWidth. Ia menjelaskan apa yang seharusnya terjadi. Namun, ia gagal menjelaskan dari mana pageWidth seharusnya berasal, dan satu kelalaian itu saja sudah cukup untuk meruntuhkan seluruh pipeline.
Dokumen arsitektur sangat baik dalam menjelaskan perilaku. Namun, mereka sering kali buruk dalam menjelaskan batasan (boundaries). Kalimat yang berbunyi "fungsi memeriksa x terhadap pageWidth" bukanlah kontrak teknis; itu adalah narasi yang menyembunyikan dependensi di dalam bahasa Inggris biasa. Ketika seorang pengembang membaca kalimat tersebut dan menulis fungsi tingkat modul yang mereferensikan pageWidth berdasarkan namanya, kode tersebut tampak benar karena memenuhi deskripsi tersebut. Kemudian runtime mencoba menyelesaikan nama tersebut, tidak menemukan apa pun dalam scope, dan akhirnya melempar error.
Refactor yang Seharusnya Sederhana
Saya melihat pola yang persis sama ini saat melakukan refactor pada perakitan halaman (page assembly). Spesifikasi tersebut mencantumkan dua persyaratan yang tampak bersih:
- Periksa
FEATURE_LAYOUT. - Bandingkan setiap region dengan
pageWidth / 2.
Pengembang mengikuti instruksi tersebut dengan setia. Mereka mengekstrak fungsi utilitas pada lingkup modul (module scope) dan memasukkan pageWidth langsung ke dalam bodi fungsi tanpa menetapkannya sebagai parameter. Spesifikasi tersebut tidak menyatakan bahwa pageWidth harus datang melalui daftar argumen. Ia juga tidak menyatakan bahwa fungsi tersebut berada pada lingkup modul, di mana pageWidth tidak lagi terlihat. Ia hanya berasumsi bahwa pelaksana memahami konteks eksekusi secara implisit.
Hasilnya adalah ReferenceError pada setiap pemuatan PDF. Karena variabel tersebut tidak ada dalam lingkup modul, fungsi tersebut langsung melempar error. Seandainya spesifikasi tersebut secara eksplisit menyebutkan batasan fungsi dan inputnya, pengembang pasti akan memasukkan pageWidth, dan bug tersebut secara struktural tidak akan mungkin terjadi. Sebaliknya, instruksi tersebut justru bertindak seperti jebakan, mengundang pelaksana untuk menjangkau ke lingkup induk (parent scope) yang sebenarnya tidak ada.
Empat Makna dari "Uses"
Masalah yang lebih mendalam adalah bahwa prosa tidak memiliki sistem tipe (type system). Ketika sebuah spesifikasi mengatakan "fungsi menggunakan X," kalimat tersebut ambigu dalam setidaknya empat cara spesifik dalam codebase JavaScript modern:
- Fungsi menerima X sebagai parameter formal.
- Fungsi membaca X dari variabel tingkat modul yang dideklarasikan di file yang sama.
- Fungsi melakukan closure terhadap X dari lingkup induk yang bersarang (nested parent scope).
- Fungsi mengekstrak X dari objek yang lebih besar yang diteruskan ke dalamnya.
Setiap opsi ini memenuhi redaksi spesifikasi. Setiap opsi ini lolos analisis statis (static analysis). Namun hanya satu yang benar untuk batasan tertentu, dan pilihan yang salah membocorkan asumsi melintasi batasan tersebut dengan cara yang terkompilasi secara diam-diam.
Pengembang biasanya memilih jalan termudah saat menulis kode. Jika pageWidth kebetulan berada di lingkup luar, mereka akan membacanya dari sana daripada mengubah tanda tangan fungsi (function signature). Sebuah closure menyembunyikan dependensi tersebut. Kode tersebut berjalan pada eksekusi pertama, lolos suite pengujian, dan dirilis. Beberapa minggu kemudian, seseorang memindahkan fungsi yang sama ke file berbeda untuk penggunaan kembali, atau untuk meningkatkan keterbacaan. Lingkup induknya pun hilang. Kode tersebut rusak, dan kerusakan tersebut tampak seperti regresi baru padahal penyebab utamanya adalah dependensi tersembunyi yang asli.
Web Workers Menghapus Bukti
Masalah ini menjadi benar-benar berbahaya begitu Web Workers masuk ke dalam arsitektur. Ketika terjadi kesalahan di dalam worker, browser akan menghapus informasi yang paling Anda butuhkan.
Inilah yang sebenarnya terjadi. Di dalam worker, pengecualian yang tidak tertangkap (uncaught exception) memicu ErrorEvent. Jika worker meneruskan error tersebut ke main thread, pola tipikalnya adalah mengambil string message dan mengirimkannya melintasi batasan. Main thread menerima string tersebut, membuat objek Error baru darinya, lalu mencatat atau melemparnya kembali. Apa yang muncul di DevTools adalah error yang telah dikonstruksi ulang di dalam handler pesan main thread. Nama file asli, nomor baris, dan stack trace dibuang. Lokasi sebenarnya dari kegagalan tersebut menjadi tidak terlihat.
Jadi, ketika pageWidth yang hilang memicu ReferenceError di dalam worker, main thread hanya melaporkan teks "pageWidth is not defined" pada lokasi di mana pesan tersebut ditangani. Fungsi tingkat modul yang sebenarnya berada di
