Node mengelola I/O secara non-blocking sehingga operasi yang belum selesai perlu menyerahkan kendali ke event loop dan melanjutkan pekerjaan lewat callback. Pola penanganan hasil I/O telah melalui tiga tahap evolusi: callback manual, promise sebagai wadah nilai masa depan, dan async/await yang menulis kode async dengan gaya sinkron. Topik ini menyusuri ketiganya beserta penanganan error pada tiap tahap, lalu membandingkan kapan komposisi menjadi sulit dan kapan blok try/catch tunggal cukup.
Pola callback dan konvensi error-first#
Callback adalah fungsi yang diteruskan sebagai argumen ke fungsi async, lalu dipanggil ketika hasil tersedia. Node menetapkan konvensi error-first: parameter pertama callback selalu err, dan parameter berikutnya memuat hasil. Konvensi ini mengikat seluruh modul inti Node seperti fs.readFile dan dns.lookup.
function ambil(nomor, delay, cb) {
setTimeout(() => cb(`data #${nomor}`), delay);
}
// Callback bersarang (callback hell)
ambil(1, 20, (a) => {
ambil(2, 20, (b) => {
ambil(3, 20, (c) => {
console.log("callback bersarang:", [a, b, c]);
});
});
});callback bersarang: [ 'data #1', 'data #2', 'data #3' ]Pola ini bekerja untuk satu langkah, tetapi komposisi tiga langkah berurutan menghasilkan piramida yang menjorok ke kanan. Setiap langkah baru menambah satu tingkat indentasi, sehingga alur data sulit ditelusuri dan penanganan error terpencar di banyak tempat. Karena callback tidak mengembalikan nilai yang dapat diperiksa pemanggil, pemanggil tidak memiliki cara untuk menyatakan “lakukan B setelah A berhasil” tanpa menyematkan logika B di dalam A.
Promise sebagai wadah nilai masa depan#
Promise adalah objek yang merepresentasikan nilai yang belum tersedia, dengan tiga keadaan: pending, fulfilled, dan rejected. Sekali promise fulfilled atau rejected, keadaannya tidak berubah lagi. Komposisi dilakukan lewat .then dan .catch, keduanya mengembalikan promise baru sehingga rantai dapat dilanjutkan tanpa menjorok. Berkas callback-vs-promise.js membandingkan ketiga pola dalam satu titik pemanggilan.
// Pola error-first callback
function ambilCallback(nomor, delay, gagal, cb) {
setTimeout(() => {
if (gagal) cb(new Error(`gagal #${nomor}`));
else cb(null, `data #${nomor}`);
}, delay);
}
ambilCallback(1, 30, false, (err, data) => {
if (err) {
console.log("callback error:", err.message);
return;
}
console.log("callback hasil:", data);
});
// Pola promise yang menggantikan callback
function ambilPromise(nomor, delay, gagal) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (gagal) reject(new Error(`gagal #${nomor}`));
else resolve(`data #${nomor}`);
}, delay);
});
}
ambilPromise(2, 20, false)
.then((data) => console.log("promise hasil:", data))
.catch((err) => console.log("promise error:", err.message));
// Pola async/await yang membungkus promise
async function jalankan() {
try {
const data = await ambilPromise(3, 10, false);
console.log("async/await hasil:", data);
} catch (err) {
console.log("async/await error:", err.message);
}
}
jalankan();async/await hasil: data #3
promise hasil: data #2
callback hasil: data #1Keluaran tidak berurut sesuai nomor karena tiap pemanggilan memiliki delay yang berbeda: callback ambilCallback(1, 30) selesai paling akhir, sedangkan await ambilPromise(3, 10) selesai paling awal. Fakta ini menjadi peringatan bahwa urutan baris kode tidak menentukan urutan hasil pada program async.
async/await: promise dengan gaya sinkron#
async/await adalah sintaks yang membungkus promise sehingga kode async dapat ditulis seperti kode sinkron. Fungsi async selalu mengembalikan promise, sedangkan await menunda eksekusi sampai promise di sebelah kanannya settled. Karena await bekerja pada promise, modul berbasis callback perlu dibungkus dulu dengan util.promisify agar dapat di-await.
Keuntungan utama async/await adalah komposisi linear. Tiga langkah berurutan tidak lagi menjorok, dan penanganan error terkumpul dalam satu blok try/catch alih-alih tersebar pada banyak .catch per langkah. Berkas async-await-error.js menunjukkan satu try/catch yang menutup banyak await.
function ambil(nomor, delay, gagal = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (gagal) reject(new Error(`gagal #${nomor}`));
else resolve(`data #${nomor}`);
}, delay);
});
}
// Penanganan error konsisten: satu try/catch menangkap semua reject
async function jalankan() {
try {
const a = await ambil(1, 20);
const b = await ambil(2, 10, true);
const c = await ambil(3, 30);
console.log("semua berhasil:", [a, b, c]);
} catch (err) {
console.log("tertangkap:", err.message);
}
}
jalankan();tertangkap: gagal #2Reject yang dilempar pada ambil(2, 10, true) langsung melompat ke catch, sehingga ambil(3, 30) tidak pernah dipanggil. Perilaku ini sama dengan Promise.all yang berhenti pada kegagalan pertama, dan menjadi dasar pemilihan pola konkurensi.
Penanganan error pada tiap tahap#
Setiap tahap evolusi memiliki disiplin penanganan error yang berbeda. Tabel berikut membandingkan titik tangkap dan keterbatasannya.
| Tahap | Titik tangkap error | Keterbatasan |
|---|---|---|
| Callback | Parameter err pada setiap callback | Mudah terlewat, tersebar di banyak tempat |
.then/.catch | Satu .catch di akhir rantai | Cabang paralel perlu .catch sendiri |
async/await | Satu try/catch menutup banyak await | Loop forEach tidak meng-await tanpa pembungkus |
Rantai promise versus await berurutan#
Rantai .then dan await berurutan saling setara secara semantik: keduanya menunggu promise sebelumnya settled sebelum memulai langkah berikutnya. Pilihan antara keduanya biasanya bergantung pada gaya yang lebih mudah dibaca untuk kasus tertentu. Rantai .then tetap berguna ketika transformasi bersifat deklaratif, misalnya memetakan nilai melalui beberapa fungsi murni tanpa percabangan.
Bentuk rantai ambil(1, 20).then((a) => ambil(2, 20).then((b) => [a, b])).then(([a, b]) => console.log(a, b)) dan bentuk await berurutan const a = await ambil(1, 20); const b = await ambil(2, 20); menghasilkan keluaran yang sama. Karena await menghentikan eksekusi sampai promise settled, dua await berurutan menjadi operasi serial dan total waktu adalah penjumlahan durasi kedua operasi. Bila kedua operasi tidak saling bergantung, sebut saja pengambilan dua berkas konfigurasi yang berbeda, gunakan Promise.all agar keduanya berjalan paralel. Kombinator lain seperti Promise.allSettled dan Promise.race menyediakan kebijakan penanganan kegagalan yang berbeda untuk kasus paralel.
Kesalahan umum#
Lupa return setelah menangani error di callback. Tanpa return, kode setelah blok if (err) tetap berjalan dan mengakses data yang tidak terdefinisi, sehingga error asli tertutup oleh TypeError baru yang lebih membingungkan.
Mengabaikan promise yang ditolak tanpa .catch. Promise yang rejected dan tidak tertangkap memunculkan unhandled rejection yang pada Node versi modern mengakhiri proses. Selalu akhiri rantai dengan .catch atau bungkus await di dalam try/catch.
Ringkasan#
- Konvensi callback Node adalah error-first: parameter pertama selalu
err. - Promise menyatukan keberhasilan dan kegagalan ke dalam satu objek dengan keadaan final yang tidak berubah, sehingga komposisi dapat dirantai tanpa menjorok.
async/awaitmenulis ulang rantai promise menjadi blok linear dengan satutry/catchuntuk banyakawait.forEachtidak meng-awaitcallback async, sehinggafor...ofatauPromise.allmenjadi pilihan yang aman.
Referensi#
- Using Promises — MDN — Panduan komposisi promise dan penanganan error rantai.
- async function — MDN
— Sintaks
async/awaitdan perilakuawaitpada promise yang rejected. - util.promisify(original) — Node.js — Fungsi untuk mengubah callback error-first menjadi promise.