Kenapa ini penting
Banyak pekerjaan di aplikasi membutuhkan waktu yang tidak bisa dipastikan: mengambil data dari server, membaca file, menunggu respons pembayaran. Jika JavaScript menunggu dengan cara memblokir (diam sampai data datang), seluruh halaman akan membeku. Tombol tidak bisa diklik, animasi berhenti. Karena JavaScript hanya punya satu thread utama, ia harus asynchronous: memulai pekerjaan, lanjut mengerjakan hal lain, lalu diberi tahu saat pekerjaan selesai.
Bayangkan kamu memesan makanan di food court. Kasir memberimu buzzer (alat getar). Kamu tidak berdiri menunggu di depan dapur, kamu duduk, mengobrol, bermain HP. Ketika makanan siap, buzzer bergetar. Jika ada masalah (bahan habis), buzzer juga memberi tahu. Promise adalah buzzer itu: object yang mewakili hasil yang akan datang, entah berhasil atau gagal.
Promise adalah dasar dari fetch, async/await, dan hampir semua API modern. Menguasainya wajib sebelum kamu menyentuh framework apa pun.
Konsep inti
Dari callback ke callback hell
Cara lama menangani asinkron adalah callback: function yang diberikan untuk dipanggil nanti. Masalah muncul saat langkah-langkah saling bergantung:
login(user, (err, sesi) => {
if (err) return tampilkanError(err);
ambilProfil(sesi, (err, profil) => {
if (err) return tampilkanError(err);
ambilTransaksi(profil.id, (err, data) => {
if (err) return tampilkanError(err);
// ... makin ke kanan
});
});
});
Inilah callback hell atau "pyramid of doom": kode menjorok ke kanan, penanganan error diulang di setiap level, dan alurnya sulit dibaca.
Tiga state Promise
| State | Arti | Berubah ke |
|---|---|---|
pending |
Masih menunggu | fulfilled atau rejected |
fulfilled |
Berhasil, punya value | Tidak berubah lagi |
rejected |
Gagal, punya reason (biasanya Error) | Tidak berubah lagi |
Promise yang sudah fulfilled atau rejected disebut settled, dan statusnya tidak akan berubah lagi.
then, catch, finally
.then(onFulfilled)dijalankan saat berhasil, menerima value..catch(onRejected)dijalankan saat gagal, menerima reason..finally(fn)dijalankan dalam kedua kasus, tanpa argumen.
Yang membuat Promise hebat: setiap .then mengembalikan Promise baru. Apa pun yang kamu return di dalam .then menjadi value Promise berikutnya. Jika yang di-return adalah Promise, rantai akan menunggunya. Hasilnya, langkah berurutan bisa ditulis datar (chaining), dan satu .catch di akhir menangkap error dari langkah mana pun.
Membuat Promise sendiri
const p = new Promise((resolve, reject) => {
// kerjakan sesuatu...
if (berhasil) resolve(nilai);
else reject(new Error("alasan"));
});
Function yang diberikan ke new Promise disebut executor dan dijalankan langsung (sinkron). Hanya panggilan resolve/reject pertama yang berlaku. Ada juga jalan pintas Promise.resolve(nilai) dan Promise.reject(err).
Pola paling umum adalah promisify: membungkus API berbasis callback atau timer menjadi function yang mengembalikan Promise, misalnya tunggu(ms).
Contoh kode
// 1. Promise sendiri: tunggu beberapa ms
function tunggu(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// 2. Simulasi API yang bisa gagal
const DB_USER = { sari: { id: 7, nama: "Sari Dewi" } };
const DB_TRANSAKSI = { 7: [{ ket: "Gaji", jumlah: 8000000 }, { ket: "Kos", jumlah: -1500000 }] };
function login(username) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = DB_USER[username];
if (user) resolve(user);
else reject(new Error(`User "${username}" tidak terdaftar`));
}, 100);
});
}
function ambilTransaksi(userId) {
return tunggu(100).then(() => DB_TRANSAKSI[userId] ?? []);
}
// 3. Chaining datar: tidak ada piramida
console.log("Memulai...");
login("sari")
.then((user) => {
console.log("Login sebagai", user.nama);
return ambilTransaksi(user.id); // return Promise → rantai menunggu
})
.then((daftar) => {
const saldo = daftar.reduce((s, t) => s + t.jumlah, 0);
console.log(`Saldo: Rp${saldo.toLocaleString("id-ID")}`);
return saldo; // return nilai biasa → jadi value then berikutnya
})
.then((saldo) => console.log(saldo > 5000000 ? "Kondisi sehat" : "Perlu berhemat"))
.catch((err) => console.log("Gagal:", err.message)) // menangkap error dari langkah mana pun
.finally(() => console.log("Proses login selesai"));
console.log("Kode ini jalan duluan, tidak menunggu");
// → Memulai...
// → Kode ini jalan duluan, tidak menunggu
// → Login sebagai Sari Dewi
// → Saldo: Rp6.500.000
// → Kondisi sehat
// → Proses login selesai
// 4. Jalur gagal
tunggu(400).then(() =>
login("budi")
.then((user) => ambilTransaksi(user.id)) // dilewati karena login gagal
.then(() => console.log("Tidak akan tampil"))
.catch((err) => console.log("Gagal:", err.message))
);
// → Gagal: User "budi" tidak terdaftar
// 5. Error yang dilempar di dalam then juga ditangkap catch
tunggu(600)
.then(() => {
throw new Error("Kurs tidak valid");
})
.catch((err) => {
console.log("Ditangkap:", err.message);
return "pakai kurs cadangan"; // catch juga bisa memulihkan rantai
})
.then((pesan) => console.log("Lanjut dengan", pesan));
// → Ditangkap: Kurs tidak valid
// → Lanjut dengan pakai kurs cadangan
// 6. Executor berjalan sinkron & hanya resolve pertama yang berlaku
const p = new Promise((resolve, reject) => {
console.log("Executor langsung jalan");
resolve("pertama");
resolve("kedua"); // diabaikan
reject(new Error("x")); // diabaikan
});
tunggu(800).then(() => p.then((v) => console.log("Value:", v)));
// → Executor langsung jalan (muncul paling awal saat file dijalankan)
// → Value: pertama
// 7. Promisify fungsi bergaya callback (err, hasil)
function bacaDataLama(id, callback) {
setTimeout(() => (id > 0 ? callback(null, { id, isi: "data lama" }) : callback(new Error("id salah"))), 50);
}
function bacaData(id) {
return new Promise((resolve, reject) => {
bacaDataLama(id, (err, hasil) => (err ? reject(err) : resolve(hasil)));
});
}
tunggu(1000).then(() => bacaData(5)).then((d) => console.log(d)); // → { id: 5, isi: 'data lama' }
Perhatikan urutan output di contoh 3: "Kode ini jalan duluan" muncul sebelum hasil login. Promise tidak membuat kode menunggu; ia hanya menjadwalkan apa yang harus dikerjakan ketika hasilnya datang. Node.js menyediakan util.promisify untuk melakukan contoh 7 secara otomatis, dan banyak API Node modern sudah punya versi Promise (misalnya fs/promises).
Kesalahan umum
- Lupa
returndi dalam.then. Tanpa return,thenberikutnya menerimaundefineddan tidak menunggu Promise di dalamnya. Ini bug Promise nomor satu. - Menyarangkan then di dalam then (callback hell versi Promise). Kembalikan Promise dan lanjutkan rantainya secara datar.
- Tidak ada
.catch. Penolakan yang tidak ditangani memunculkan Unhandled Promise Rejection; di Node.js modern ini menghentikan proses. - Mengira kode setelah Promise menunggu hasilnya.
let data; ambil().then(d => data = d); console.log(data)mencetakundefined. Pakai data di dalamthen(atauawait). - Membungkus Promise dengan
new Promisetanpa perlu (explicit construction anti-pattern). Jika function sudah mengembalikan Promise, langsung return saja.
Latihan
- Mudah, Buat
lemparKoin()yang mengembalikan Promise: setelah 500 ms, resolve"Angka"atau rejectError("Gambar")secara acak. Tangani keduanya. - Sedang, Ubah alur callback hell
login → ambilProfil → ambilTransaksimenjadi rantai Promise datar dengan satu.catch. Buat ketiga function sebagai Promise simulasi. - Menantang, Tulis
denganTimeout(promise, ms)yang reject denganError("Timeout")jikapromisebelum settled dalammsmilidetik. Gunakannew PromisedansetTimeout(tanpaPromise.race).
Lihat solusi
// 1
const lemparKoin = () =>
new Promise((resolve, reject) =>
setTimeout(() => (Math.random() < 0.5 ? resolve("Angka") : reject(new Error("Gambar"))), 500));
lemparKoin()
.then((hasil) => console.log("Menang:", hasil))
.catch((err) => console.log("Kalah:", err.message)); // → Menang: Angka atau Kalah: Gambar
// 2
const jeda = (ms, nilai) => new Promise((r) => setTimeout(() => r(nilai), ms));
const masuk = (u) => (u ? jeda(50, { token: "t-1" }) : Promise.reject(new Error("Username kosong")));
const ambilProfil = (sesi) => jeda(50, { id: 7, token: sesi.token });
const ambilTrx = (profil) => jeda(50, [{ jumlah: 100000 }, { jumlah: -20000 }]);
masuk("sari")
.then(ambilProfil)
.then(ambilTrx)
.then((trx) => console.log("Jumlah transaksi:", trx.length)) // → Jumlah transaksi: 2
.catch((e) => console.log("Error:", e.message));
// 3
function denganTimeout(promise, ms) {
return new Promise((resolve, reject) => {
const id = setTimeout(() => reject(new Error("Timeout")), ms);
promise.then(
(v) => { clearTimeout(id); resolve(v); },
(e) => { clearTimeout(id); reject(e); },
);
});
}
denganTimeout(jeda(1000, "lambat"), 200).catch((e) => console.log(e.message)); // → Timeout
denganTimeout(jeda(100, "cepat"), 200).then((v) => console.log(v)); // → cepat
Ringkasan
- Asinkron mencegah aplikasi membeku saat menunggu pekerjaan lama; callback bersarang memicu callback hell.
- Promise punya tiga state:
pending,fulfilled,rejected; sekali settled tidak berubah. .thenmengembalikan Promise baru, jadi langkah berurutan bisa di-chain secara datar.- Satu
.catchdi akhir menangkap error dari seluruh rantai;.finallyuntuk bersih-bersih. new Promise((resolve, reject) => ...)untuk membungkus timer/callback; executor berjalan sinkron.