Menu

Promise JavaScript: Penjelasan then, catch, dan Promise.all

Cara kerja Promise di JavaScript: tiga state-nya, chaining dengan then dan catch, menggabungkan dengan Promise.all, dan membuat sendiri dengan new Promise.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Promise adalah pengganti sementara untuk nilai di masa depan

Saat JavaScript perlu melakukan sesuatu yang memakan waktu, seperti request jaringan, membaca file, atau menunggu timer, ia tidak bisa langsung memberimu hasilnya. Sebagai gantinya, ia memberimu Promise: object yang mewakili nilai yang akan ada, cepat atau lambat.

console.log pertama menampilkan Promise yang masih pending. Setengah detik kemudian, Promise itu resolve, dan callback .then berjalan dengan nilainya. Promise itu sendiri hanyalah object; keajaibannya ada pada kemampuannya memberi tahu siapa pun yang mendengarkan saat nilainya tiba.

Tiga state

Promise selalu berada di salah satu dari tiga state:

  • pending: pekerjaannya sedang berjalan. Belum ada nilai.
  • fulfilled: pekerjaannya berhasil. Nilainya tersedia.
  • rejected: pekerjaannya gagal. Error-nya tersedia.

Promise berpindah dari pending ke fulfilled atau rejected tepat satu kali, lalu menetap di sana selamanya. Kamu tidak bisa membatalkan resolve sebuah Promise atau me-resolve-nya dua kali.

Promise.resolve(value) membuat Promise yang sudah fulfilled; Promise.reject(error) membuat Promise yang sudah rejected. Praktis untuk testing dan untuk mengembalikan Promise dari fungsi yang kadang sudah punya jawabannya saat itu juga.

Membaca nilai: .then dan .catch

Kamu tidak membongkar Promise secara langsung. Kamu mengirim callback ke .then, dan Promise memanggilnya saat nilainya siap:

.catch(fn) berjalan kalau Promise reject. Di balik layar, ia adalah singkatan dari .then(undefined, fn). .catch() di ujung rantai menangani rejection dari langkah mana pun di atasnya; kamu tidak perlu memasangnya setelah setiap .then.

Chaining: setiap .then mengembalikan Promise baru

Bagian inilah yang sering menjebak orang. .then() tidak hanya menjalankan callback: ia mengembalikan Promise baru yang resolve dengan apa pun yang dikembalikan callback itu. Itulah yang memungkinkan chaining:

Setiap langkah mengisi langkah berikutnya. Kalau callback .then mengembalikan Promise, rantainya menunggu Promise itu sebelum lanjut, sehingga langkah-langkah async tersusun dengan rapi:

Tiga langkah async berurutan, tanpa bersarang. Bandingkan dengan logika yang sama yang ditulis memakai callback, dan kamu akan mengerti kenapa Promise jadi populer.

Error mengalir menembus rantai

Promise yang rejected melompati setiap .then sampai menemukan .catch. Itulah seluruh model penanganan error-nya:

Melempar error di dalam .then membuat Promise yang dikembalikan .then itu reject. .then berikutnya melihat rejection itu dan meneruskannya, sampai .catch menelannya. Satu .catch di ujung rantai biasanya sudah cukup, dan rantai tanpa .catch sama sekali akan menghasilkan peringatan "unhandled promise rejection", yang sebaiknya kamu perbaiki.

Membuat sendiri dengan new Promise

Sebagian besar waktu, kamu akan memakai Promise yang diberikan library. Sesekali kamu perlu membungkus sesuatu yang tidak mengembalikan Promise, biasanya API callback gaya lama:

Fungsi yang kamu kirim ke new Promise disebut executor. Ia menerima dua argumen: resolve (panggil dengan nilai keberhasilan) dan reject (panggil dengan error). Panggil salah satunya tepat satu kali. Setelah itu, pemanggilan berikutnya diabaikan.

Dua kebiasaan yang menyelamatkanmu dari masalah:

  • Pakai new Promise hanya saat membungkus sesuatu yang belum berbasis Promise. Kalau sebuah fungsi sudah mengembalikan Promise, cukup kembalikan saja.
  • Selalu reject dengan object Error, bukan string. Stack trace layak dipertahankan.

Menjalankan secara paralel: Promise.all

Rantai .then berjalan berurutan. Saat kamu punya beberapa tugas async yang tidak saling bergantung dan ingin menjalankannya bersamaan, Promise.all adalah alatnya:

Ketiga timer berjalan bersamaan. Promise.all resolve dengan array berisi hasil dalam urutan yang sama dengan input, setelah setiap Promise fulfilled. Total waktunya kira-kira 400ms, bukan 900ms.

Masalahnya: Promise.all langsung reject begitu salah satu Promise-nya reject, dan hasil lainnya hilang. Itu perilaku yang tepat saat kamu butuh semua bagiannya (misalnya me-render halaman yang membutuhkan tiga panggilan API). Kalau tidak, pakai allSettled.

Saat sebagian kegagalan tidak masalah: Promise.allSettled

Promise.allSettled menunggu setiap Promise selesai, entah fulfilled atau rejected, lalu memberimu laporan:

Setiap hasil berupa object: { status: "fulfilled", value } atau { status: "rejected", reason }. Berguna saat keberhasilan sebagian bisa diterima: mencatat sekumpulan event, mengambil banyak thumbnail, menjalankan health check yang tidak saling bergantung.

Dua combinator lain yang perlu kamu tahu:

  • Promise.race([...]): selesai begitu Promise pertama selesai, berhasil atau gagal. Praktis untuk timeout.
  • Promise.any([...]): fulfilled dengan keberhasilan pertama dan mengabaikan rejection. Hanya reject kalau setiap Promise reject.

Promise selalu async

Bahkan Promise yang sudah resolve memanggil callback .then-nya secara asynchronous, tidak pernah secara synchronous, tidak pernah di tick yang sama:

Output-nya adalah before, after, immediate. Callback .then menunggu kode saat ini selesai, lalu berjalan di microtask queue. Aturan ini, "callback Promise tidak pernah berjalan secara synchronous", adalah alasan kenapa mencampur Promise dengan kode synchronous bisa diprediksi: kode synchronous selalu selesai lebih dulu.

Berikutnya: async/await

Merangkai pemanggilan .then memang berhasil, tapi begitu langkahnya lebih dari dua atau tiga, bentuknya mulai mirip tangga. async/await adalah sintaks di atas Promise yang memungkinkanmu menulis logika yang sama seolah-olah synchronous, dengan try/catch untuk error dan variabel biasa untuk nilai perantara. Itulah berikutnya.

Pertanyaan yang sering diajukan

Apa itu Promise di JavaScript?

Promise adalah object yang mewakili nilai yang belum siap, biasanya hasil di masa depan dari operasi async seperti request jaringan. Ia selalu berada di salah satu dari tiga state: pending, fulfilled, atau rejected. Kamu membaca nilai akhirnya dengan memasang callback lewat .then() dan .catch().

Apa bedanya then dan catch?

.then(onFulfilled) berjalan saat Promise berhasil resolve dan menerima nilai hasil resolve-nya. .catch(onRejected) berjalan saat Promise itu (atau Promise mana pun sebelumnya di rantai) reject dan menerima error-nya. Satu .catch() di ujung rantai menangani kegagalan dari langkah mana pun di atasnya.

Apa fungsi Promise.all?

Promise.all([p1, p2, p3]) menerima array berisi Promise dan mengembalikan satu Promise yang fulfilled dengan array berisi semua nilai hasil resolve, tapi baru setelah setiap Promise input fulfilled. Kalau ada satu saja yang reject, semuanya langsung reject. Pakai Promise.allSettled kalau kamu ingin setiap hasil terlepas dari ada yang gagal atau tidak.

Sebaiknya pakai Promise atau async/await?

Keduanya mesin yang sama: async/await adalah sintaks di atas Promise. Kebanyakan kode baru lebih mudah dibaca dengan async/await, tapi kamu tetap mengembalikan Promise, tetap menangkap error dengan try/catch atau .catch(), dan tetap memakai Promise.all untuk menjalankan sesuatu secara paralel. Memahami cara kerja Promise membuat async/await jadi masuk akal.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai