Pengguna etalase toko mengirim tangkapan layar berisi laporan kesalahan: baris galatnya menunjuk galat.min.js:1:146. Siti membuka berkas itu dan menemukan satu baris teks yang membentang ratusan kolom, penuh nama variabel singkat seperti n, e, dan o. Galat yang sesungguhnya ada di galat.js baris 3 — tetapi tidak ada petunjuk apa pun yang menghubungkan keduanya.

Inilah persoalan yang dijawab source map: berkas peta yang menyimpan hubungan antara setiap posisi di kode hasil build dan posisi asalnya di kode sumber. Dengan peta itu, DevTools di browser maupun Node dapat menampilkan stack trace yang menunjuk baris dan nama asli. Chapter ini merekonstruksi laporan Siti langkah demi langkah: melihat masalahnya, membaca isi peta, lalu mengaktifkan pemetaannya.

Masalah: Kode Hasil Build Sulit Dilacak #

Berkas sumber kita sengaja dibuat ringkas — sebuah fungsi yang melempar Error bila pembaginya nol:

js galat.js
function bagiTotal(total, jumlahOrang) {
  if (jumlahOrang === 0) {
    throw new Error("pembagi tidak boleh nol");
  }
  return total / jumlahOrang;
}
console.log(bagiTotal(150000, 0));

Bundler esbuild (dibahas penuh di chapter berikutnya) mengompilasi berkas ini menjadi versi mini:

bash
npx --yes esbuild@latest galat.js --bundle --minify --sourcemap --outfile=dist/galat.min.js

Output yang Dihasilkan:

text
  dist/galat.min.js      266b 
  dist/galat.min.js.map  524b 

⚡ Done in 2ms

Dua berkas tercipta di folder dist. Yang pertama, galat.min.js, isinya satu baris panjang — nama fungsi dipangkas menjadi r, spasi dihilangkan, dan kode bantu bundler ditambahkan:

js dist/galat.min.js (satu baris)
(()=>{var n=(e,o)=>()=>{try{return o||e((o={exports:{}}).exports,o),o.exports}catch(i){throw o=0,i}};var t=n(()=>{function r(e,o){if(o===0)throw new Error("pembagi tidak boleh nol");return e/o}console.log(r(15e4,0))});t();})();
//# sourceMappingURL=galat.min.js.map

Menjalankannya di Node memperlihatkan stack trace yang persis seperti keluhan pengguna:

bash
node dist/galat.min.js

Output yang Dihasilkan:

text
/tmp/tooling-final/dist/galat.min.js:1
(()=>{var n=(e,o)=>()=>{try{return o||e((o={exports:{}}).exports,o),o.exports}catch(i){throw o=0,i}};var t=n(()=>{function r(e,o){if(o===0)throw new Error("pembagi tidak boleh nol");return e/o}console.log(r(15e4,0))});t();})();
                                                                                       ^

Error: pembagi tidak boleh nol
    at r (/tmp/tooling-final/dist/galat.min.js:1:146)
    at /tmp/tooling-final/dist/galat.min.js:1:206
    at /tmp/tooling-final/dist/galat.min.js:1:39
    at /tmp/tooling-final/dist/galat.min.js:1:219
    at Object.<anonymous> (/tmp/tooling-final/dist/galat.min.js:1:225)
    at Module._compile (node:internal/modules/cjs/loader:1929:14)
    at Object..js (node:internal/modules/cjs/loader:2060:10)
    at Module.load (node:internal/modules/cjs/loader:1651:32)
    at Module._load (node:internal/modules/cjs/loader:1443:12)
    at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)

Node.js v24.21.0

Frame pertama, at r (...galat.min.js:1:146), membaca nama r dan posisi kolom 146 — teknisnya benar, tetapi tidak mengatakan apa-apa tentang bagiTotal di galat.js. Tanpa peta, inilah yang dilihat debugger.

Anatomi Berkas .map #

Berkas kedua hasil build, galat.min.js.map, adalah teks JSON. Berikut isi lengkapnya — alat yang membuatnya adalah esbuild; peta tidak pernah ditulis tangan:

json dist/galat.min.js.map
{
  "version": 3,
  "sources": ["../galat.js"],
  "sourcesContent": ["function bagiTotal(total, jumlahOrang) {\n  if (jumlahOrang === 0) {\n    throw new Error(\"pembagi tidak boleh nol\");\n  }\n  return total / jumlahOrang;\n}\nconsole.log(bagiTotal(150000, 0));\n"],
  "mappings": "qGAAA,IAAAA,EAAAC,EAAA,cAASC,EAAUC,EAAOC,EAAa,CACrC,GAAIA,IAAgB,EAClB,MAAM,IAAI,MAAM,yBAAyB,EAE3C,OAAOD,EAAQC,CACjB,CACA,QAAQ,IAAIF,EAAU,KAAQ,CAAC,CAAC",
  "names": ["require_galat", "__commonJSMin", "bagiTotal", "total", "jumlahOrang"]
}
  1. "version": 3: nomor spesifikasi format peta yang dipakai — versi 3 adalah format standar yang dibaca semua alat modern.
  2. "sources": daftar berkas sumber, ditulis relatif terhadap folder peta. "../galat.js" berarti satu folder di atas dist/.
  3. "sourcesContent": isi lengkap berkas sumber, disematkan ke dalam peta. Karena itu debugger bisa menampilkan kode asli tanpa mengunduh ulang berkas sumbernya.
  4. "mappings": inti dari peta — rangkaian angka terenkode yang menghubungkan setiap posisi di berkas hasil build dengan baris-kolom di berkas sumber. Enkodenya disebut base64 VLQ: nilai-nilai kecil dikemas rapat agar ukuran peta tetap hemat. Kita tidak pernah menghitungnya manual; debugger yang menerjemahkannya.
  5. "names": kumpulan nama variabel dan fungsi asli yang dirujuk pemetaan — dari daftar inilah nama bagiTotal dipulihkan dari r.

//# sourceMappingURL: Tautan di Ujung Berkas #

Bagaimana debugger tahu bahwa peta tersedia? Jawabannya ada di baris terakhir galat.min.js:

js
//# sourceMappingURL=galat.min.js.map

Komentar khusus ini adalah kontrak baku: nama berkas peta ditulis relatif terhadap folder berkas yang memuatnya. Saat DevTools browser memuat galat.min.js, ia membaca komentar tersebut, mengambil galat.min.js.map, dan menyediakan pemetaan secara otomatis — tanpa pengaturan apa pun dari kita. Di browser, panel Sources DevTools kemudian menampilkan galat.js yang asli, lengkap dengan breakpoint di baris aslinya. Node melakukan hal yang sama setelah diaktifkan.

Membaca Stack Trace yang Terpetakan #

Node membaca peta bila dijalankan dengan flag --enable-source-maps. Berkasnya sama persis dengan percobaan pertama — hanya flag yang berubah:

bash
node --enable-source-maps dist/galat.min.js

Output yang Dihasilkan:

text
/tmp/tooling-final/dist/galat.min.js:1
(()=>{var n=(e,o)=>()=>{try{return o||e((o={exports:{}}).exports,o),o.exports}catch(i){throw o=0,i}};var t=n(()=>{function r(e,o){if(o===0)throw new Error("pembagi tidak boleh nol");return e/o}console.log(r(15e4,0))});t();})();
                                                                                       ^

Error: pembagi tidak boleh nol
    at bagiTotal (/tmp/tooling-final/galat.js:3:11)
    at <anonymous> (/tmp/tooling-final/galat.js:7:13)
    at /tmp/tooling-final/dist/galat.min.js:1:39
    at bagiTotal (/tmp/tooling-final/galat.js:7:33)
    at Object.<anonymous> (/tmp/tooling-final/galat.js:7:33)
    at Module._compile (node:internal/modules/cjs/loader:1929:14)
    at Object..js (node:internal/modules/cjs/loader:2060:10)
    at Module.load (node:internal/modules/cjs/loader:1651:32)
    at Module._load (node:internal/modules/cjs/loader:1443:12)
    at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)

Node.js v24.21.0

Laporan yang sama kini terbaca seperti kode sumbernya:

  1. at bagiTotal (/tmp/tooling-final/galat.js:3:11): frame yang tadinya at r ... :1:146 kini menampilkan nama asli bagiTotal, berkas asli galat.js, dan baris 3 — persis baris throw new Error(...) tempat galat dilempar. Node membaca mappings, menerjemahkan posisi kolom 146, dan mengganti tampilannya.
  2. at <anonymous> (galat.js:7:13): frame pemanggil juga terpetakan — baris 7 berisi console.log(bagiTotal(...)), tempat kejadian dari sisi pemanggil.
  3. at ...dist/galat.min.js:1:39: satu frame tetap menunjuk berkas hasil build. Frame itu berada di kode bantu bundler yang tidak punya padanan langsung di berkas sumber, sehingga peta tidak mengubahnya. Stack trace nyata sering berisi campuran seperti ini — yang penting frame milik kode kita sudah terbaca.

Satu pertimbangan menutup chapter ini: peta yang dipublikasikan bersama kode hasil build membawa sourcesContent — artinya kode sumber asli ikut terbaca oleh siapa pun yang membukanya. Untuk aplikasi publik, tim biasanya mengunggah peta hanya ke layanan pelacak galat, bukan ke folder yang bisa diakses publik.

Yang kita jalankan tadi — perintah esbuild yang menghasilkan dist/ beserta petanya — adalah kerja seorang bundler. Chapter berikutnya membedah perannya secara utuh.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

galat.js

Unduh berkas
javascript
function bagiTotal(total, jumlahOrang) {
  if (jumlahOrang === 0) {
    throw new Error("pembagi tidak boleh nol");
  }
  return total / jumlahOrang;
}
console.log(bagiTotal(150000, 0));