Sebelum Promise dan async/await ada, seluruh kode asynchronous Node.js bertumpu pada satu pola: meneruskan sebuah function sebagai argumen yang dipanggil kembali ketika operasi selesai — itulah callback. Pola ini masih sangat relevan hingga kini karena mayoritas API core modules dan banyak library lama memakainya, dan Promise sendiri dibangun di atas konsep penanganan hasil yang sama. Chapter ini membahas konvensi callback yang aman, masalah klasik callback hell, serta cara menjembatani function callback lama ke dunia Promise.

Konvensi Error-First #

Node.js menetapkan konvensi baku untuk callback: parameter pertama selalu reservasi untuk error, parameter kedua dan seterusnya membawa hasil. Ketika operasi berhasil, parameter pertama diisi null; ketika gagal, parameter pertama berisi object Error dan hasil diisi null. Konvensi ini disebut error-first callback dan berlaku di hampir seluruh API asynchronous Node.js.

Tiga aturan mainnya perlu kita pegang:

  • periksa parameter err sebelum memakai hasil;
  • callback dipanggil tepat satu kali — sekali untuk error atau sekali untuk sukses, tidak pernah keduanya;
  • jangan mengabaikan err — error yang tidak diperiksa baru muncul sebagai kegagalan di tempat yang jauh dari sumber masalahnya.

Berikut implementasi function pembungkus yang mengikuti konvensi tersebut:

js
import { readFile } from "node:fs";

function bacaDaftarTugas(namaBerkas, callback) {
  readFile(namaBerkas, "utf8", (err, isi) => {
    if (err) {
      callback(err, null);
      return;
    }
    try {
      callback(null, JSON.parse(isi));
    } catch (e) {
      callback(e, null);
    }
  });
}

bacaDaftarTugas("tugas.json", (err, daftar) => {
  if (err) {
    console.log(`Gagal membaca daftar tugas: ${err.message}`);
    return;
  }
  console.log(`Jumlah tugas: ${daftar.length}`);
  console.log(`Tugas pertama: ${daftar[0].judul}`);
});

Output yang Dihasilkan:

text
Jumlah tugas: 3
Tugas pertama: Belajar event loop

Mari kita bedah baris-per-baris function di atas:

  1. Baris 3-14 (bacaDaftarTugas): function menerima nama berkas dan callback, lalu mendelegasikan pembacaan ke fs.readFile — API bawaan yang juga error-first.
  2. Baris 5-8: baca berkas gagal, callback pemanggil dipanggil dengan (err, null) dan return menghentikan eksekusi agar cabang sukses tidak ikut berjalan. return setelah memanggil error adalah penjaga agar callback tidak pernah dipanggil dua kali.
  3. Baris 9-13: isi berkas di-parse sebagai JSON. Parsing bisa melempar error secara sinkron, sehingga dibungkus try/catch dan dialihkan ke callback sebagai error — konsumen tetap menerima error lewat satu pintu yang sama.
  4. Baris 16-23: pemanggil memeriksa err paling awal, menghentikan alur bila ada masalah, baru membaca hasilnya dengan aman.

Apa yang terjadi bila kita lalai memeriksa err? Operasi gagal diam-diam, parameter hasil berisi undefined, dan error baru muncul beberapa baris kemudian sebagai TypeError yang menyesatkan. Studi kasus lengkapnya dibedah pada Chapter Error pada Kode Asynchronous.

Callback Hell #

Masalah klasik pola callback muncul ketika operasi asynchronous harus berurutan: setiap langkah menempatkan langkah berikutnya di dalam callback-nya, dan kode menjalar ke kanan membentuk piramida:

js
function tunda(ms, pesan, callback) {
  setTimeout(() => callback(null, pesan), ms);
}

tunda(10, "ambil data pengguna", (err, langkah1) => {
  tunda(20, "ambil daftar tugas", (err, langkah2) => {
    tunda(30, "ambil riwayat tugas", (err, langkah3) => {
      console.log(langkah1);
      console.log(langkah2);
      console.log(langkah3);
    });
  });
});

Output yang Dihasilkan:

text
ambil data pengguna
ambil daftar tugas
ambil riwayat tugas

Function tunda di baris 1-3 adalah simulasi operasi I/O dengan jeda tetap: 10, 20, lalu 30 milidetik. Karena setiap langkah menunggu langkah sebelumnya, callback bersarang tiga tingkat — dan ini baru tiga langkah. Pada kode nyata dengan lima sampai sepuluh langkah, piramida seperti ini membuat alur program sulit diikuti, penanganan error harus diulang di tiap tingkat, dan mengubah urutan langkah berarti memindahkan blok kode secara utuh.

Menjembatani Callback ke Promise #

Untuk function lama yang mengikuti konvensi error-first, Node menyediakan util.promisify() yang membungkusnya menjadi function penghasil Promise secara otomatis. Versi rapi dari piramida di atas terlihat seperti ini:

js
import { promisify } from "node:util";

function tunda(ms, pesan, callback) {
  setTimeout(() => callback(null, pesan), ms);
}

const tundaPromise = promisify(tunda);

const langkah1 = await tundaPromise(10, "ambil data pengguna");
const langkah2 = await tundaPromise(20, "ambil daftar tugas");
const langkah3 = await tundaPromise(30, "ambil riwayat tugas");

console.log(langkah1);
console.log(langkah2);
console.log(langkah3);

Output yang Dihasilkan:

text
ambil data pengguna
ambil daftar tugas
ambil riwayat tugas

Kedua program mencetak hasil yang persis sama, tetapi versi Promise datar: tiga langkah berjalan berurutan di level yang sama tanpa penyarangan. Syarat promisify() sederhana — function asli menerima callback di posisi terakhir dengan tanda tangan error-first — persis seperti tunda kita. API bawaan seperti fs.readFile juga memenuhi syarat ini, sehingga promisify(readFile) langsung bisa dipakai.

AspekCallback error-firstPromise + await
Representasi errorParameter pertama callbacktry/catch di sekitar await
Langkah berurutanBersarang ke dalam (piramida)Berurutan dari atas ke bawah
Callback dipanggil dua kaliRisiko bila penulis lalaiTidak mungkin

Dengan Promise di tangan, langkah berurutan hanyalah satu pola dari banyak. Bagaimana menjalankan banyak operasi sekaligus dan menggabungkan hasilnya — termasuk ketika sebagian gagal — adalah tugas empat combinator yang dibedah pada Chapter Promise untuk Tugas Paralel.