Siti sedang membangun fitur cek status pendaftaran untuk lokakaryanya: pengunjung memasukkan nomor pendaftaran, lalu halaman menampilkan nama dan sesi yang dipilih. Data pendaftar disimpan di server, dan mengambilnya butuh waktu — tidak ada yang tahu kapan data tiba. Pertanyaannya, bagaimana menulis kode yang hasilnya belum tersedia saat barisnya dieksekusi?
Jawaban tertua, dan masih dipakai sampai sekarang, adalah callback: function yang kita titipkan untuk dipanggil belakangan, saat hasilnya siap. Kita sudah mengenal callback sinkron di section Functions — function yang dikirim ke map dan dipanggil dalam putaran yang sama. Chapter ini membahas callback yang dipanggil belakangan: pola error-first ala Node.js, masalah yang muncul ketika panggilan wajib bersarang, dan arah solusi modernnya.
Callback yang Dipanggil Belakangan #
Callback asinkron menambah satu kontrak pada pola yang sudah dikenal: kita tidak hanya menitipkan function, tetapi juga menyerahkan kendali waktu pemanggilannya kepada pihak lain. Pemanggil — misalnya pembaca berkas atau klien jaringan — menjalankan pekerjaan lambatnya sendiri, dan begitu selesai, callback yang kita titip dipanggil membawa hasilnya. Thread utama tidak pernah menunggu, sesuai perilaku non-blocking yang kita buktikan di awal section.
Bedanya dengan callback sinkron ada pada saat pemanggilan. Callback di dalam map dipanggil dalam putaran yang sama — sebelum map selesai. Callback asinkron baru dipanggil setelah pekerjaan lambat selesai, entah itu beberapa milidetik atau beberapa detik kemudian, dan selalu lewat antrean event loop. Contoh yang akan kita temui di course ini:
setTimeoutdansetIntervalmemanggil callback pada waktunya (chapter Timers);- listener event memanggil callback saat pengguna berinteraksi (section Browser Events);
- function pembaca berkas Node.js memanggil callback setelah berkas selesai dibaca.
Karena pekerjaan asinkron bisa gagal, callback butuh jalur untuk melaporkan kegagalan. Ekosistem Node.js menyetujui satu konvensi yang disebut error-first callback:
- Parameter pertama callback selalu
error: bernilainullbila sukses, berisi objekErrorbila gagal. - Parameter berikutnya adalah hasil, satu per satu, dan hanya relevan bila
errorbernilainull. - Pemanggil selalu memeriksa
errorterlebih dahulu sebelum menyentuh hasil.
Konvensi ini membuat seluruh function asinkron Node.js — dari fs.readFile hingga modul pihak ketiga — punya bentuk yang seragam dan mudah dikenali.
Pola Error-First ala Node.js #
Berkas berikut meniru satu tabel “database” kecil. Function ambilPendaftar menerima id dan callback, lalu memanggil callback itu 20 milidetik kemudian — di jalur sukses atau di jalur gagal:
// callback-node-style.js
// Pola callback error-first: callback(error, hasil).
// --- 1. simulasi "database" pendaftar lokakarya ---
const dataPendaftar = {
"101": { nama: "Budi", sesi: "Dasar JavaScript" },
"102": { nama: "Siti", sesi: "Asynchronous" },
};
function ambilPendaftar(id, callback) {
setTimeout(() => {
if (dataPendaftar[id]) {
callback(null, dataPendaftar[id]);
} else {
callback(new Error(`Pendaftar ${id} tidak ditemukan`));
}
}, 20);
}
// --- 2. pemakaian: periksa error dulu, baru pakai data ---
ambilPendaftar("101", (error, pendaftar) => {
if (error) {
console.log("gagal:", error.message);
return;
}
console.log(`ditemukan: ${pendaftar.nama} — sesi ${pendaftar.sesi}`);
});
// --- 3. jalur gagal: id yang tidak terdaftar ---
ambilPendaftar("999", (error, pendaftar) => {
if (error) {
console.log("gagal:", error.message);
return;
}
console.log(`ditemukan: ${pendaftar.nama}`);
});
console.log("permintaan dikirim, hasil menyusul lewat callback");
// --- 4. callback hell: tiga langkah yang saling bergantung ---
ambilPendaftar("101", (error, pendaftar) => {
if (error) return console.log("langkah 1 gagal:", error.message);
console.log("langkah 1:", pendaftar.nama);
ambilPendaftar("102", (error, pendaftarKedua) => {
if (error) return console.log("langkah 2 gagal:", error.message);
console.log("langkah 2:", pendaftarKedua.nama);
ambilPendaftar("103", (error, pendaftarKetiga) => {
if (error) return console.log("langkah 3 gagal:", error.message);
console.log("langkah 3:", pendaftarKetiga.nama);
});
});
});Output yang Dihasilkan:
permintaan dikirim, hasil menyusul lewat callback
ditemukan: Budi — sesi Dasar JavaScript
gagal: Pendaftar 999 tidak ditemukan
langkah 1: Budi
langkah 2: Siti
langkah 3 gagal: Pendaftar 103 tidak ditemukanMari kita telaah baris-baris pentingnya:
- Baris 10–18 (
ambilPendaftar): signature-nya persis pola Node.js — parameter kedua adalah callback, dan pemanggilan callback terjadi di dalam callbacksetTimeout, jadi 20 milidetik kemudian. - Baris 13 (
callback(null, dataPendaftar[id])): jalur sukses. Nilai pertamanullberarti “tidak ada galat”, dan data pendaftar menjadi parameter kedua. - Baris 15 (
callback(new Error(...))): jalur gagal. ObjekErrordikirim lewat parameter pertama; parameter hasil tidak dikirim sama sekali. - Baris 21–27 (pemanggil pertama): urutan baku pemakai callback — periksa
errordulu (baris 22), keluar cepat bila ada, baru gunakan data (baris 26). - Baris 38 (log sinkron): tercetak sebelum semua callback karena callback baru mendapat giliran lewat antrean macrotask, seperti dibuktikan di chapter Event Loop.
- Baris 41–52 (bagian 4): tiga pencarian berurutan yang setiap langkahnya butuh id langkah berikutnya; kita bedah di bawah.
Callback Hell: Ketika Langkah Saling Bergantung #
Bagian 4 berkas di atas menampilkan masalah paling terkenal dari pola callback. Tiga pencarian saling bergantung — langkah kedua baru boleh dimulai setelah langkah pertama selesai — sehingga callback wajib ditulis bersarang di dalam callback sebelumnya. Hasilnya tampak seperti piramida terbalik yang makin dalam.
Bayangkan bila langkahnya bertambah menjadi lima: ada lima lapis sarang, lima pemeriksaan error yang mirip satu sama lain, dan lima tingkat indentasi yang harus dipertahankan. Menyisipkan satu langkah di tengah berarti menggeser seluruh lapis di bawahnya. Pemeliharaan kode seperti ini makin berat seiring waktu.
Bentuk seperti ini disebut callback hell, dan masalahnya bukan sekadar tampilan:
- Indentasi membengkak: setiap langkah baru menambah satu lapis sarang; alur yang sebenarnya lurus menjadi sulit dibaca.
- Penanganan galat ditulis ulang di setiap lapis: satu lapis yang lupa memeriksa
errormembuat kegagalan lewat tanpa penanganan. - Sulit disusun ulang: membalik urutan, menjalankan dua langkah paralel, atau menyisipkan langkah berarti membongkar seluruh sarang.
Jalan Keluar: Promise #
Standar modern mengganti sarang callback dengan satu objek yang mewakili hasil masa depan. Konversinya pun mekanis — callback error-first cukup dibungkus sekali menjadi bentuk yang mengembalikan promise:
// Bentuk yang kita bangun tuntas di chapter Promises.
// resolve menggantikan jalur sukses, reject menggantikan jalur gagal.
function ambilPendaftarSebagaiPromise(id) {
return new Promise((resolve, reject) => {
ambilPendaftar(id, (error, pendaftar) => {
if (error) reject(error);
else resolve(pendaftar);
});
});
}Function di atas mengembalikan promise; pemanggil menyambung langkah berikutnya dengan then dan menampung kegagalan dengan catch — tanpa sarang, tanpa penulisan ulang penanganan galat. Satu bungkusan seperti ini juga membuat pekerjaan asinkron bisa digabung dengan Promise.all dan kawan-kawannya, sesuatu yang sulit dilakukan callback mentah.
Menariknya, pola bungkusan ini begitu lazim sampai Node.js menyediakan versi promise siap pakai untuk modul bawaannya, misalnya fs.promises. Kita tidak perlu membungkus sendiri function yang sudah disediakan runtime. Chapter berikutnya membedah pembuatan promise dari dasarnya — bila prinsipnya pegang, membaca maupun menulis bungkusan seperti di atas menjadi pekerjaan rutin.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
callback-node-style.js
Unduh berkas// callback-node-style.js
// Pola callback error-first: callback(error, hasil).
// --- 1. simulasi "database" pendaftar lokakarya ---
const dataPendaftar = {
"101": { nama: "Budi", sesi: "Dasar JavaScript" },
"102": { nama: "Siti", sesi: "Asynchronous" },
};
function ambilPendaftar(id, callback) {
setTimeout(() => {
if (dataPendaftar[id]) {
callback(null, dataPendaftar[id]);
} else {
callback(new Error(`Pendaftar ${id} tidak ditemukan`));
}
}, 20);
}
// --- 2. pemakaian: periksa error dulu, baru pakai data ---
ambilPendaftar("101", (error, pendaftar) => {
if (error) {
console.log("gagal:", error.message);
return;
}
console.log(`ditemukan: ${pendaftar.nama} — sesi ${pendaftar.sesi}`);
});
// --- 3. jalur gagal: id yang tidak terdaftar ---
ambilPendaftar("999", (error, pendaftar) => {
if (error) {
console.log("gagal:", error.message);
return;
}
console.log(`ditemukan: ${pendaftar.nama}`);
});
console.log("permintaan dikirim, hasil menyusul lewat callback");
// --- 4. callback hell: tiga langkah yang saling bergantung ---
ambilPendaftar("101", (error, pendaftar) => {
if (error) return console.log("langkah 1 gagal:", error.message);
console.log("langkah 1:", pendaftar.nama);
ambilPendaftar("102", (error, pendaftarKedua) => {
if (error) return console.log("langkah 2 gagal:", error.message);
console.log("langkah 2:", pendaftarKedua.nama);
ambilPendaftar("103", (error, pendaftarKetiga) => {
if (error) return console.log("langkah 3 gagal:", error.message);
console.log("langkah 3:", pendaftarKetiga.nama);
});
});
});