Sebagian besar kode yang kamu tulis sejauh ini jalan dari atas ke bawah, satu baris selesai dulu sebelum lanjut ke baris berikutnya. Tapi begitu programmu harus berurusan dengan dunia luar — mengambil data dari server, membaca file, menunggu sebuah timer — muncul masalah. Pekerjaan macam itu butuh waktu, dan JavaScript jelas tidak bisa membekukan seluruh halaman cuma demi menunggu jawabannya. Yang dilakukannya: tugas itu dijalankan, programnya tetap lanjut, lalu nanti kembali lagi setelah hasilnya siap. Gaya “balik lagi nanti” inilah yang disebut kode asynchronous, dan Promise adalah objek yang disediakan JavaScript untuk mengelolanya tanpa bikin kodemu jadi simpang siur.
Kalau kamu sudah kenal fungsi dan pernah belajar soal penanganan error, bekalmu sudah cukup. Promise justru mengikat kedua konsep itu jadi satu.
Apa sih sebenarnya “asynchronous” itu
Bayangkan kamu memesan kopi di kafe. Kamu tidak berdiri mematung di depan kasir sambil melototi mesin sampai kopinya jadi — kamu dikasih pager kecil, lalu menyingkir ke meja, dan pager itu bergetar begitu pesananmu siap. Sementara itu kafenya tetap melayani pelanggan lain.
Begitulah inti kerja asynchronous. JavaScript memulai sesuatu yang lambat (kopinya), memberimu penanda untuk hasil yang nanti datang (pager-nya), lalu lanjut mengerjakan hal lain. Setelah yang lambat tadi rampung, kamu dikabari. Alternatifnya — kode synchronous yang memblokir segalanya sampai selesai — ibarat tiap pelanggan membekukan seluruh kafe sampai pesanannya jadi. Jelas tidak masuk akal.
Nah, Promise itu si pager tadi. Ia objek yang mewakili sebuah nilai yang belum kamu pegang sekarang, tapi pasti akan kamu dapatkan suatu saat — entah berupa keberhasilan, entah berupa kegagalan.
Sebelum ada Promise: callback hell
Sebelum Promise ada, cara menangani “lakukan ini setelah yang lambat tadi selesai” adalah lewat callback — sebuah fungsi yang kamu oper untuk dijalankan belakangan:
getUser(1, function (user) {
getOrders(user, function (orders) {
getOrderDetails(orders[0], function (details) {
console.log(details);
});
});
});
Tiap langkah bergantung pada langkah sebelumnya, jadi fungsinya bersarang makin dalam. Begitu ditambah penanganan error di tiap level, kode ini cepat berubah jadi tangga menjorok yang nyaris mustahil dibaca — para programmer menjulukinya “callback hell” (atau “pyramid of doom”). Promise diciptakan justru untuk meratakan kembali tumpukan ini jadi sesuatu yang benar-benar enak dibaca.
Bentuk sebuah Promise
Sebuah Promise selalu berada di salah satu dari tiga state:
- Pending — pekerjaannya masih berjalan, hasilnya belum ada. (Kopinya masih dibuat.)
- Fulfilled — pekerjaannya berhasil dan nilainya sudah ada. (Kopimu siap.)
- Rejected — pekerjaannya gagal, dan ada alasannya (biasanya berupa error). (Kopinya kehabisan biji.)
Promise dimulai dari pending, lalu settle (mengendap) tepat satu kali — entah jadi fulfilled, entah jadi rejected. Begitu sudah settle, statusnya terkunci; ia tidak akan pernah berubah state lagi ataupun jalan dua kali. Jaminan “hanya sekali” inilah yang jadi salah satu alasan kenapa Promise lebih gampang dipahami dibanding callback mentah.
Kamu lebih sering memakai Promise, bukan membuatnya
Ini bagian yang melegakan: dalam kode sehari-hari, kamu jarang sekali bikin Promise dari nol. Browser dan kebanyakan library sudah menyodorkan Promise yang siap pakai — fetch() mengembalikan Promise, begitu juga banyak API lain. Jadi tugasmu yang sebenarnya, sebagian besar waktu, adalah tahu cara memakai sebuah Promise: cara mengambil hasilnya saat berhasil dan menangani error-nya saat gagal. Karena itu, fokuskan dulu energimu ke .then() dan .catch(); bikin Promise sendiri datang belakangan dan lebih jarang dipakai.
Memakai Promise: .then() dan .catch()
Begitu kamu pegang sebuah Promise, kamu menempelkan handler padanya. Method .then() jalan ketika Promise-nya fulfilled, dan ia menerima nilai hasilnya. Method .catch() jalan ketika Promise-nya rejected, dan ia menerima error-nya:
fetch("https://api.example.com/user/1")
.then(function (response) {
console.log("Dapat respons!", response);
})
.catch(function (error) {
console.log("Ada yang salah:", error);
});
Bacanya hampir seperti kalimat biasa: jalankan fetch, lalu then kalau berhasil jalankan ini, tapi kalau gagal catch error-nya di sini. Kode setelah blok ini tetap jalan duluan — .then() dan .catch() baru menyala nanti, setelah panggilan jaringannya settle.
Dengan arrow function, bacanya jadi makin ringkas, dan beginilah bentuk yang paling sering kamu temui:
fetch("https://api.example.com/user/1")
.then((response) => console.log("Dapat respons!", response))
.catch((error) => console.log("Ada yang salah:", error));
.finally() — jalan apa pun hasilnya
Ada handler ketiga, yaitu .finally(), yang jalan begitu Promise-nya settle apa pun hasilnya — berhasil maupun gagal. Ini pas banget untuk pembersihan yang memang harus selalu terjadi, misalnya menyembunyikan loading spinner:
showSpinner();
fetch("https://api.example.com/data")
.then((response) => console.log("Selesai:", response))
.catch((error) => console.log("Gagal:", error))
.finally(() => hideSpinner()); // jalan baik berhasil maupun gagal
Chaining: kekuatan utamanya
Hal terpenting dari .then() adalah ini: ia mengembalikan Promise baru. Artinya kamu bisa memanggil .then() lagi di atas hasil .then() sebelumnya, sehingga operasi-operasi bisa dirangkai satu per satu secara berurutan. Apa pun yang kamu return di dalam sebuah .then() akan jadi nilai yang diterima .then() berikutnya:
fetch("https://api.example.com/user/1")
.then((response) => response.json()) // mengembalikan Promise berisi body yang sudah di-parse
.then((user) => {
console.log("User:", user.name);
return user.id; // nilai biasa, dioper ke bawah
})
.then((id) => console.log("ID user:", id))
.catch((error) => console.log("Error:", error));
Inilah pengganti yang rata dan enak dibaca untuk piramida callback bersarang tadi. Tiap langkah cuma satu baris di tingkat indentasi yang sama, terbaca dari atas ke bawah persis seperti resep masakan. Perhatikan, response.json() itu sendiri juga mengembalikan Promise — dan rangkaiannya menangani ini otomatis. Kalau sebuah .then() mengembalikan Promise, rangkaiannya akan menunggu Promise itu settle dulu sebelum lanjut. Satu aturan inilah yang bikin pengurutan langkah-langkah async jadi sebersih ini.
Satu .catch() di ujung menangkap seluruh rangkaian
Kamu tidak perlu menaruh .catch() di belakang tiap-tiap .then(). Error di dalam rangkaian Promise itu menjalar ke bawah — kalau ada satu langkah yang rejected (atau melempar error), JavaScript bakal melewati sisa handler .then() dan langsung lompat ke .catch() terdekat berikutnya. Jadi satu .catch() di dasar rangkaian sudah cukup untuk menangani kegagalan dari langkah mana pun di atasnya. Ini benar-benar peningkatan dibanding callback, yang mengharuskanmu menangani error secara manual di tiap level.
Membuat Promise sendiri
Kadang kamu memang perlu bikin Promise sendiri — paling sering untuk membungkus API lama yang masih berbasis callback, atau untuk menunggu sesuatu seperti timer. Kamu pakai konstruktor new Promise(). Konstruktor ini menerima sebuah fungsi dengan dua parameter, yang secara konvensi dinamai resolve dan reject. Panggil resolve(nilai) untuk menjadikannya fulfilled, atau reject(error) untuk menjadikannya rejected:
function wait(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve(`Menunggu ${ms}ms`), ms);
});
}
wait(1000).then((pesan) => console.log(pesan)); // setelah 1 detik: "Menunggu 1000ms"
Di sini wait mengembalikan Promise yang tetap pending sampai timer-nya menyala, lalu resolve dengan sebuah pesan. Contoh yang lebih mendekati nyata membungkus operasi yang bisa gagal:
function cekUmur(umur) {
return new Promise((resolve, reject) => {
if (umur >= 18) {
resolve("Akses diberikan");
} else {
reject(new Error("Kamu harus berusia 18 tahun ke atas"));
}
});
}
cekUmur(20)
.then((pesan) => console.log(pesan)) // "Akses diberikan"
.catch((error) => console.log(error.message));
cekUmur(15)
.then((pesan) => console.log(pesan))
.catch((error) => console.log(error.message)); // "Kamu harus berusia 18 tahun ke atas"
Reject dengan Error, dan jangan lupa .catch()-nya
Dua kebiasaan ini bakal menyelamatkanmu dari banyak pusing. Pertama, saat kamu reject, oper objek new Error(...) yang benar, bukan sekadar string mentah — objek Error membawa stack trace dan .message, yang bikin proses debugging jauh lebih gampang. Kedua, selalu pasang .catch() di suatu titik pada rangkaiannya. Promise yang rejected tapi tidak punya .catch() bakal memunculkan “unhandled promise rejection” — kegagalannya terjadi diam-diam di latar belakang, dan kamu cuma bengong menatap kode yang… ya pokoknya nggak jalan, tanpa petunjuk jelas kenapa. Setiap rangkaian yang bisa gagal wajib punya handler error.
Menjalankan beberapa Promise sekaligus
Sering kali kamu punya beberapa tugas async yang saling independen, dan kamu tidak mau menunggunya satu per satu secara bergantian. Objek Promise punya method bantuan untuk mengoordinasi sekelompok Promise sekaligus. Empat yang wajib kamu tahu:
Promise.all — tunggu semuanya (gagal kalau ada satu yang gagal)
Promise.all() menerima array berisi Promise dan mengembalikan satu Promise yang fulfilled dengan array berisi semua hasilnya — tapi hanya setelah semuanya berhasil. Kalau ada satu saja yang rejected, seluruhnya langsung ikut rejected:
const p1 = fetch("/api/user").then((r) => r.json());
const p2 = fetch("/api/posts").then((r) => r.json());
const p3 = fetch("/api/comments").then((r) => r.json());
Promise.all([p1, p2, p3])
.then(([user, posts, comments]) => {
console.log(user, posts, comments); // ketiganya, sesuai urutan
})
.catch((error) => console.log("Setidaknya ada satu yang gagal:", error));
Ini pilihan utama saat kamu butuh seluruh datanya dulu baru bisa lanjut, dan permintaan-permintaannya tidak saling bergantung — semuanya jalan paralel, jadi jauh lebih cepat ketimbang dikerjakan berurutan.
Promise.allSettled — tunggu semuanya (tidak pernah gagal)
Kadang kamu ingin tahu hasil tiap Promise sekalipun ada yang gagal. Promise.allSettled() tidak pernah rejected; ia menunggu semuanya settle lalu menyodorkan array yang menggambarkan tiap Promise — status-nya ("fulfilled" atau "rejected") plus value atau reason:
Promise.allSettled([p1, p2, p3]).then((results) => {
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("OK:", result.value);
} else {
console.log("Gagal:", result.reason);
}
});
});
Promise.race — yang pertama settle, dia menang
Promise.race() settle begitu Promise pertama settle, dengan hasil dari Promise itu — entah ia fulfilled entah rejected. Pemakaian klasiknya adalah memasang timeout untuk permintaan yang lambat:
const data = fetch("/api/slow");
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Kehabisan waktu")), 5000)
);
Promise.race([data, timeout])
.then((response) => console.log("Dapat tepat waktu"))
.catch((error) => console.log(error.message)); // "Kehabisan waktu" kalau fetch-nya kelamaan
Promise.any — yang pertama berhasil, dia menang
Promise.any() mirip race, tapi ia mengabaikan yang rejected dan menunggu yang pertama fulfilled. Ia baru rejected kalau semua Promise-nya gagal. Berguna kalau kamu punya beberapa mirror dan cuma ingin mengambil yang merespons paling cepat:
Promise.any([fetch("/mirror1"), fetch("/mirror2"), fetch("/mirror3")])
.then((response) => console.log("Sukses pertama:", response))
.catch((error) => console.log("Semua mirror gagal"));
Contekan singkat: all vs allSettled vs race vs any
Cara cepat membedakannya: all = “Aku butuh semua hasilnya, dan gagalkan begitu ada satu yang gagal.” allSettled = “Kasih tahu aku hasil semuanya, bagus maupun gagal.” race = “Siapa pun yang selesai duluan, berhasil atau gagal.” any = “Siapa pun yang berhasil duluan.” Sebagian besar waktu pakai all, pakai allSettled kalau gagal sebagian masih bisa diterima, dan race/any untuk timeout serta fallback.
Jebakan yang sering kejadian: lupa return
Di dalam sebuah .then(), kalau kamu memulai operasi async tapi lupa me-return Promise-nya, rangkaiannya tidak akan menunggu operasi itu. Ia ngebut jalan duluan, dan timing-mu jadi berantakan dengan cara yang bikin bingung:
// SALAH — rangkaiannya tidak menunggu fetch di dalam
fetch("/api/user")
.then((response) => response.json())
.then((user) => {
fetch(`/api/orders/${user.id}`); // tidak ada return! rangkaian langsung lanjut
})
.then(() => console.log("Selesai")); // tercetak sebelum order selesai diambil
// BENAR — return Promise-nya supaya rangkaiannya menunggu
fetch("/api/user")
.then((response) => response.json())
.then((user) => {
return fetch(`/api/orders/${user.id}`); // di-return → rangkaian menunggu
})
.then(() => console.log("Selesai"));
Setiap kali kamu mengerjakan tugas async di dalam sebuah .then(), return Promise-nya. Lupa return termasuk bug Promise yang paling sering muncul, dan celakanya ia gagal diam-diam, bukan melempar error yang kelihatan jelas.
Lanjutan dari Promise: async/await
Rangkaian Promise sudah jauh lebih baik dibanding callback, tapi rangkaian .then() yang panjang tetap bisa terasa agak padat. JavaScript modern menambahkan satu lapisan sintaks di atas Promise yang bernama async/await, yang membuatmu bisa menulis kode asynchronous yang terbaca seperti kode synchronous biasa:
async function loadUser() {
try {
const response = await fetch("/api/user/1");
const user = await response.json();
console.log(user.name);
} catch (error) {
console.log("Error:", error);
}
}
Perhatikan, tidak ada satu pun .then() di sini — await menghentikan sementara fungsinya sampai Promise-nya settle, lalu langsung menyerahkan nilainya. Tapi ini poin pentingnya: async/await itu sepenuhnya dibangun di atas Promise. await hanya bekerja pada sebuah Promise, dan fungsi async selalu mengembalikan Promise. Semua yang kamu pelajari di sini tetap berlaku di baliknya — dan justru itulah alasan kenapa Promise harus dipelajari lebih dulu. Itu jadi bahasan artikel berikutnya.
Penutup
Sekarang kamu sudah paham bagaimana JavaScript menangani pekerjaan yang butuh waktu:
- Kode asynchronous memulai tugas yang lambat, lanjut mengerjakan hal lain, dan kembali lagi setelah tugas itu selesai — bukannya membekukan segalanya. Promise adalah penanda untuk hasil yang nanti datang itu.
- Sebuah Promise selalu berada di state pending, fulfilled, atau rejected, dan ia settle tepat satu kali.
- Pakai sebuah Promise dengan
.then()(berhasil),.catch()(gagal), dan.finally()(selalu jalan). Ini bakal jauh lebih sering kamu lakukan ketimbang membuat Promise sendiri. - Chaining bisa berjalan karena
.then()mengembalikan Promise baru. Return sebuah nilai (atau Promise) untuk mengopernya ke bawah rangkaian, dan satu.catch()di ujung sudah menangani error dari langkah mana pun. - Buat Promise dengan
new Promise((resolve, reject) => ...)— biasanya untuk membungkus API callback lama. Reject denganErroryang benar, dan jangan pernah meninggalkan rangkaian tanpa.catch(). - Koordinasikan sekelompok Promise dengan
Promise.all(semua berhasil),allSettled(semua hasilnya),race(yang pertama settle), danany(yang pertama berhasil).
Promise adalah fondasi dari setiap pola asynchronous modern di JavaScript. Setelah kamu nyaman dengan .then(), .catch(), dan chaining di sini, sintaks async/await di artikel berikutnya bakal terasa seperti lapisan cat yang lebih ramah di atas mesin yang persis sama.