| (gambar ilustrasi di atas: dua request async yang dikirim berurutan tapi tiba dengan urutan terbalik, menyebabkan state akhir UI menampilkan data yang salah) |
Pernah mengalami bug aneh di mana hasil pencarian di aplikasi Anda menampilkan data dari query lama, padahal pengguna sudah mengetik kata kunci baru? Atau tombol submit yang di-klik dua kali cepat malah menyimpan data yang salah? Selamat datang di dunia race condition — salah satu bug paling licin dalam pemrograman asynchronous JavaScript.
Apa Itu Race Condition, Sebenarnya?
Race condition terjadi ketika hasil akhir dari sebuah program bergantung pada urutan eksekusi operasi asynchronous yang seharusnya tidak deterministik. Dalam konteks JavaScript, ini paling sering muncul saat beberapa request API dikirim hampir bersamaan, tapi responsnya bisa tiba dalam urutan yang tidak sesuai dengan urutan pengiriman.
Bayangkan skenario ini:
async function searchUser(query) {
const results = await fetch(`/api/search?q=${query}`);
const data = await results.json();
renderResults(data); // langsung render begitu respons tiba
}
// Pengguna mengetik cepat: "a" lalu "ap" lalu "app"
searchUser("a");
searchUser("ap");
searchUser("app");
Idealnya, hasil pencarian untuk kata "app" yang tampil di layar. Tapi karena setiap request punya waktu respons berbeda-beda (tergantung beban server, ukuran data, jaringan), bisa saja request untuk "a" — yang dikirim paling awal — justru tiba paling akhir dan menimpa hasil pencarian "app" yang sudah benar.
Kenapa Ini Bisa Terjadi di Bahasa "Single-Threaded"?
Ini pertanyaan yang wajar muncul. JavaScript memang single-threaded, tapi operasi asynchronous seperti fetch() dijalankan di luar thread utama (lewat Web APIs), dan waktu penyelesaiannya tidak bisa dijamin urut. Begitu masing-masing selesai, mereka masuk antre di Callback Queue atau Microtask Queue, lalu dieksekusi begitu Call Stack kosong — tanpa memedulikan urutan pengiriman aslinya.
Solusi #1: Membatalkan Request Lama dengan AbortController
Cara paling bersih untuk mengatasi ini adalah membatalkan request sebelumnya begitu request baru dikirim, menggunakan AbortController bawaan browser:
let controller;
async function searchUser(query) {
if (controller) controller.abort(); // batalkan request sebelumnya
controller = new AbortController();
try {
const results = await fetch(`/api/search?q=${query}`, {
signal: controller.signal
});
const data = await results.json();
renderResults(data);
} catch (err) {
if (err.name !== "AbortError") throw err;
}
}
Dengan pendekatan ini, hanya request terakhir yang benar-benar sempat selesai dan merender hasilnya — request-request sebelumnya otomatis dibatalkan sebelum sempat "mengganggu" state aplikasi.
Solusi #2: Menandai Request dengan ID Unik
Jika API yang dipakai tidak mendukung pembatalan, alternatifnya adalah menandai setiap request dengan penanda unik, lalu mengabaikan respons yang bukan berasal dari request paling akhir:
let latestRequestId = 0;
async function searchUser(query) {
const requestId = ++latestRequestId;
const results = await fetch(`/api/search?q=${query}`);
const data = await results.json();
// hanya render jika ini masih request terbaru
if (requestId === latestRequestId) {
renderResults(data);
}
}
Pola ini sangat berguna terutama saat bekerja dengan library atau API pihak ketiga yang tidak menyediakan mekanisme cancel bawaan.
Solusi #3: Debouncing untuk Mengurangi Jumlah Request
Untuk kasus seperti fitur pencarian real-time, seringkali solusi paling efektif justru mencegah race condition muncul sejak awal — dengan menunda pengiriman request sampai pengguna berhenti mengetik sejenak (debounce):
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const debouncedSearch = debounce(searchUser, 300);
Debouncing tidak sepenuhnya menghilangkan race condition, tapi secara drastis mengurangi kemungkinan terjadinya karena jumlah request yang dikirim jauh lebih sedikit.
Race Condition di Luar Konteks Fetch
Race condition juga bisa muncul dalam skenario lain, seperti:
- Double submission — pengguna klik tombol submit dua kali sebelum request pertama selesai, menyebabkan data terkirim ganda
- State update yang saling tumpang tindih — dua fungsi async yang sama-sama memodifikasi state global tanpa sinkronisasi
- Race antar-tab browser — dua tab yang sama-sama menulis ke
localStoragesecara bersamaan
Untuk kasus double submission, solusi sederhana biasanya cukup dengan menonaktifkan tombol sesaat setelah diklik, sampai request selesai diproses.
Checklist Sebelum Menulis Kode Asynchronous
Sebagai langkah pencegahan, ada baiknya bertanya pada diri sendiri sebelum menulis fungsi async yang memperbarui UI:
- Apakah fungsi ini bisa dipanggil berkali-kali dalam waktu singkat? Jika ya, race condition adalah risiko nyata.
- Apakah saya perlu membatalkan request sebelumnya? Gunakan
AbortControllerjika API mendukung. - Apakah cukup dengan mengabaikan respons basi? Gunakan penanda ID request jika pembatalan tidak memungkinkan.
- Apakah frekuensi pemanggilan bisa dikurangi? Pertimbangkan debounce atau throttle.
Race condition adalah salah satu bug yang sulit direproduksi secara konsisten karena sifatnya yang bergantung pada waktu — kadang muncul, kadang tidak, tergantung kecepatan jaringan saat itu. Justru karena itulah penting untuk menanganinya secara proaktif sejak awal penulisan kode, bukan menunggu sampai bug ini muncul secara acak di production dan membingungkan tim saat proses debugging.
💬 Komentar ()
Punya pertanyaan atau tanggapan? Yuk diskusi di kolom komentar.
✍ Tulis Komentar