Menu

Error Handling Async JavaScript: try/catch, Promise, dan unhandledrejection

Bagaimana error sebenarnya mengalir di JavaScript async: try/catch dengan async/await, .catch pada promise, dan jebakan yang diam-diam menelan kegagalan.

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

Error di Kode Async Tidak Bekerja Seperti Error Sync

Di JavaScript synchronous, error yang dilempar akan naik melalui call stack sampai ada try/catch yang menangkapnya, atau programnya crash. Kode async mematahkan model itu. Saat sebuah request jaringan gagal, fungsi yang memulainya sudah lama selesai. Tidak ada call stack yang bisa dinaiki.

Promise mengatasi ini dengan memberi error jalurnya sendiri. Sebuah promise bisa fulfilled dengan sebuah nilai atau rejected dengan sebuah alasan. Penolakan (rejection) adalah padanan async dari melempar error. Seluruh halaman ini membahas cara memastikan penolakan berakhir di tempat yang kamu kendalikan, bukan menghilang.

try/catch berjalan dan selesai dengan mulus. Penolakannya terjadi 50ms kemudian, lama setelah blok try selesai. Tidak ada yang menangkapnya. Itulah jebakannya.

try/catch Kembali Berfungsi dengan await

Begitu kamu await sebuah promise, penolakan berubah menjadi error yang dilempar di dalam fungsi async. try/catch di sekitarnya menangkapnya seperti throw synchronous biasa:

Inilah pola yang pertama kali harus kamu pakai. await menjembatani dunia async kembali ke bentuk try/catch yang sudah kamu kenal. Taruh panggilan await yang bisa gagal di dalam try, lalu tangani di catch.

Satu detail yang perlu dicatat: hanya panggilan yang di-await yang tercakup. Kalau kamu memulai sebuah promise tanpa menunggunya, error-nya tetap lolos.

Bug Paling Umum: Lupa await

Kalau kamu memanggil fungsi async tanpa await (atau tanpa mengembalikan promise-nya), penolakan akan lolos melewati try/catch di sekitarnya:

Blok try selesai dengan sukses. Penolakannya terjadi di tick berikutnya, tanpa ada yang menangkap. Kamu akan melihat peringatan "unhandled promise rejection" di console.

Solusinya selalu sama: await panggilannya, atau return promise-nya agar pemanggil bisa menunggunya.

.catch() Adalah Sisi Lain dari Koin yang Sama

Kamu bisa menangani penolakan tanpa async/await dengan merangkai .catch():

.catch(fn) adalah singkatan dari .then(undefined, fn). Ia menangani penolakan apa pun dari bagian rangkaian sebelumnya. .catch() di akhir rangkaian adalah padanan async dari try/catch tingkat atas: benteng terakhir sebelum penolakan menjadi "unhandled".

Mencampur kedua gaya tidak masalah. Pola yang umum adalah memakai async/await di dalam fungsi dan membiarkan pemanggilnya memasang .catch():

fetch Tidak Ditolak Saat Ada Error HTTP

Yang satu ini menjebak semua orang setidaknya sekali. fetch hanya ditolak saat ada kegagalan di tingkat jaringan: pencarian DNS gagal, koneksi ditolak, atau request dibatalkan. Response 404 atau 500 tetap dianggap fetch yang berhasil. Promise-nya selesai; hanya saja selesai dengan response yang ok-nya bernilai false.

Kalau kamu ingin error HTTP masuk ke blok catch, cek res.ok dan lempar error secara eksplisit:

Boilerplate ini layak dipindahkan ke sebuah helper begitu kamu mendapati dirimu menulisnya dua kali.

Promise.all Gagal Cepat; Promise.allSettled Tidak

Promise.all menerima array berisi promise dan selesai dengan array berisi hasil, kecuali ada yang ditolak. Dalam kasus itu, ia langsung ditolak dengan error tersebut. Promise lainnya tetap berjalan, tetapi hasilnya dibuang.

Gagal cepat adalah perilaku yang tepat saat kamu butuh setiap hasil dan satu kegagalan membuat seluruh operasi tidak berarti. Saat kamu ingin setiap hasil apa pun yang terjadi, misalnya "coba lima upload ini, beri tahu mana yang berhasil dan mana yang gagal", pakai Promise.allSettled:

allSettled tidak pernah ditolak. Setiap entri berupa {status: "fulfilled", value} atau {status: "rejected", reason}.

Melempar Ulang dan Catch yang Spesifik

Tidak semua error cocok ditangani oleh handler yang sama. Pola yang umum adalah menangkap, memeriksa, lalu melempar ulang apa pun yang tidak kamu duga:

Menelan setiap error dengan catch (err) {} kosong menyembunyikan bug sungguhan. Tangkap apa yang bisa kamu tangani dengan bermakna; lempar ulang sisanya.

Unhandled Rejection Adalah Jaring Pengamanmu

Bahkan dengan kode yang hati-hati, cepat atau lambat ada yang lolos. Node.js dan browser sama-sama menyediakan hook global untuk penolakan yang tidak ditangkap siapa pun:

// Browser
window.addEventListener("unhandledrejection", event => {
    console.error("unhandled:", event.reason);
    event.preventDefault(); // suppress the default console warning
});

// Node.js
process.on("unhandledRejection", reason => {
    console.error("unhandled:", reason);
});

Ini bukan pengganti penanganan yang benar, melainkan hook terakhir untuk log atau telemetri. Di Node.js modern, unhandled rejection menghentikan proses secara default, dan biasanya itulah yang kamu inginkan di production. Catat error-nya, lalu biarkan prosesnya mati dan mulai ulang dalam keadaan bersih.

Daftar Periksa Praktis

Saat sebuah fungsi async menyentuh apa pun yang bisa gagal, tanyakan pada dirimu:

  • Apakah setiap await yang berisiko berada di dalam try/catch, atau promise yang dikembalikan ditangani pemanggil dengan .catch()?
  • Apakah aku benar-benar melakukan await pada panggilan itu, atau tanpa sengaja menjalankannya lalu melupakannya?
  • Khusus untuk fetch, apakah aku mengecek res.ok sebelum memercayai response-nya?
  • Saat menjalankan sesuatu secara paralel, apakah Promise.all alat yang tepat, atau aku butuh Promise.allSettled?
  • Apakah ada .catch() tingkat atas atau handler unhandledrejection agar tidak ada yang hilang diam-diam?

Kalau kelima hal itu sudah benar, kode async-mu tidak akan lagi mengejutkanmu dengan error yang lenyap ke dalam event loop.

Berikutnya: ES Modules

Error handling async melengkapi bab tentang async. Berikutnya, kita beralih ke cara kode JavaScript diatur di beberapa file: import, export, dan sistem module yang menjadi fondasi setiap proyek modern.

Pertanyaan yang sering diajukan

Bagaimana cara menangani error di fungsi async?

Bungkus panggilan await dengan blok try/catch. Setiap penolakan dari promise yang di-await berubah menjadi error yang dilempar dan diterima catch. Kamu juga bisa membiarkan error-nya merambat dan menanganinya di tempat pemanggilan dengan .catch() pada promise yang dikembalikan.

Mengapa try/catch-ku tidak menangkap error-nya?

Biasanya karena error terjadi di kode yang tidak di-await. Kalau kamu memanggil fungsi async tanpa await (atau tanpa mengembalikan promise-nya), penolakan apa pun akan lolos dari try/catch yang melingkupinya. Selalu await atau return promise yang error-nya ingin kamu tangkap.

Apa yang terjadi kalau sebuah promise ditolak dan tidak ada yang menangkapnya?

Terjadilah unhandled rejection. Node.js memicu event unhandledRejection dan, di versi terbaru, menghentikan proses secara default. Browser memicu window.onunhandledrejection dan mencatat peringatan. Apa pun kasusnya, pasang .catch() atau tangani dengan try/catch di sekitar await.

Bagaimana Promise.all menangani error?

Promise.all langsung ditolak begitu salah satu promise input ditolak, sementara promise lainnya tetap berjalan tetapi hasilnya dibuang. Kalau kamu ingin semua hasil terlepas dari kegagalan, pakai Promise.allSettled: ia selesai dengan array berisi entri {status, value} atau {status, reason}.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai