Pengguna situs toko buku jarang sabar: Budi mengetik “kamus” di kolom pencarian, lalu seketika mengubahnya menjadi “kamus besar”. Tanpa pembatalan, dua permintaan berjalan — dan bila permintaan pertama datang belakangan, hasil pencarian yang tampil justru untuk kata yang sudah tidak diketik pengguna. Selain memboroskan jaringan, respons usang seperti ini mencemari antarmuka. Chapter ini membahas alat standar untuk situasi itu: AbortController, yang mengaitkan sinyal pembatalan ke fetch, termasuk bentuk siap pakainya, AbortSignal.timeout, untuk batas waktu.
Kedua berkas contoh memakai server tiruan yang sengaja lambat (400 ms) agar pembatalan sempat terjadi; karena servernya tertanam, node batalkan-permintaan.mjs dan node batas-waktu-abortsignal.mjs bisa Anda jalankan langsung untuk melihat galat yang persis sama.
signal: Menautkan Pembatalan ke fetch #
Mekanismenya dua aktor: AbortController menyimpan keadaan pembatalan, sedangkan object signal miliknya — sebuah AbortSignal — disisipkan ke options fetch. Begitu controller.abort() dipanggil, sinyal itu “menyala”, dan setiap operasi yang terikat padanya dihentikan sesegera mungkin.
const controller = new AbortController();
const response = await fetch(url, { signal: controller.signal });
// kapan pun dibutuhkan:
controller.abort();Satu sinyal bisa dibagikan ke banyak permintaan — misalnya semua permintaan hasil pencarian yang lahir dari satu kotak ketik dibatalkan serentak begitu pengguna mengetik lagi. Mesin retry di chapter berikutnya juga memakai pola ini untuk menghentikan seluruh rangkaian percobaan.
abort(): Membatalkan di Tengah Jalan #
Berkas berikut menjalankan dua skenario: pembatalan saat permintaan masih berjalan, dan pembatalan yang datang terlambat setelah respons selesai dibaca:
// batalkan-permintaan.mjs
// AbortController: membatalkan permintaan yang sedang berjalan.
// Jalankan di terminal: node batalkan-permintaan.mjs (tanpa internet).
import http from "node:http";
const server = http.createServer((req, res) => {
if (req.url === "/laporan") {
// Server tiruan sengaja lambat: 400 ms.
setTimeout(() => res.end("laporan lengkap"), 400);
return;
}
res.end("cepat");
});
server.listen(0, "127.0.0.1", async () => {
const port = server.address().port;
const dasar = `http://127.0.0.1:${port}`;
// --- 1. abort di tengah jalan: fetch ditolak dengan AbortError ---
const controller = new AbortController();
setTimeout(() => controller.abort(), 10); // batalkan setelah 10 ms
try {
await fetch(`${dasar}/laporan`, { signal: controller.signal });
console.log("selesai (tidak terjadi)");
} catch (error) {
console.log("tertangkap :", error.name, "-", error.message);
console.log("DOMException?", error instanceof DOMException);
console.log("signal.aborted:", controller.signal.aborted);
}
// --- 2. abort setelah respons selesai dibaca: tidak berpengaruh ---
const controller2 = new AbortController();
const response = await fetch(`${dasar}/cepat`, { signal: controller2.signal });
const isi = await response.text(); // body dibaca habis lebih dulu
controller2.abort();
console.log("abort belakangan:", response.status, isi, "| aborted:", controller2.signal.aborted);
server.closeAllConnections();
server.close();
});Output yang Dihasilkan:
tertangkap : AbortError - This operation was aborted
DOMException? true
signal.aborted: true
abort belakangan: 200 cepat | aborted: trueMari kita telaah baris-baris pentingnya:
- Baris 10 (
setTimeout(() => res.end(...), 400)): server tiruan menunda balasan 400 ms — jendela waktu agar pembatalan di sisi client sempat terjadi jauh sebelum respons siap. - Baris 21–22 (controller dan jadwal abort):
new AbortController()membuat pasangan controller–signal, dansetTimeoutmenjadwalkanabort()10 ms kemudian — mensimulasikan pengguna yang berubah pikiran. - Baris 25 (
{ signal: controller.signal }): sinyal ditautkan ke permintaan. Dari sini nasib permintaan terikat pada keadaan sinyal. - Baris 27–30 (penangkapan galat): pembatalan membuat
fetchreject. Jenis galatnya selalu sama:error.namebernilai"AbortError"dengan pesan"This operation was aborted", danerror instanceof DOMExceptionbernilaitrue. Pola pengecekan yang andal adalah memeriksaerror.name === "AbortError"— bukan isi pesannya. - Baris 30 (
controller.signal.aborted): sinyal mencatat keadaannya sendiri; propertiabortedbernilaitruesetelahabort()dipanggil, berguna untuk menandai permintaan mana yang dibatalkan. - Baris 35–38 (abort yang terlambat): respons sudah dibaca habis lewat
text()sebelumabort()dipanggil. Hasilnya damai: respons tetap200, isinya utuh, dan pemanggilanabort()tidak menimbulkan galat apa pun — pembatalan hanya bermakna bagi operasi yang masih berjalan.
AbortSignal.timeout: Batas Waktu Satu Baris #
Pembatalan paling sering dibutuhkan bukan karena pengguna berubah pikiran, melainkan karena server terlalu lama diam. Menyusun setTimeout + abort() manual untuk setiap permintaan melelahkan; AbortSignal.timeout(ms) memadatkannya menjadi satu panggilan — sinyal yang otomatis menyala setelah jangka waktu habis.
Bedanya terletak pada jenis galat yang ditimbulkan: pembatalan lewat waktu menghasilkan TimeoutError, bukan AbortError, sehingga kode bisa membedakan “pengguna membatalkan” dari “server tidak selesai tepat waktu”:
// batas-waktu-abortsignal.mjs
// AbortSignal.timeout: batas waktu otomatis tanpa setTimeout manual.
// Jalankan di terminal: node batas-waktu-abortsignal.mjs (tanpa internet).
import http from "node:http";
const server = http.createServer((req, res) => {
if (req.url === "/lambat") {
setTimeout(() => res.end("akhirnya selesai"), 400); // jauh di atas batas waktu
return;
}
res.end("data langsung tersedia");
});
server.listen(0, "127.0.0.1", async () => {
const port = server.address().port;
const dasar = `http://127.0.0.1:${port}`;
// --- 1. server terlalu lambat: ditolak dengan TimeoutError ---
try {
await fetch(`${dasar}/lambat`, { signal: AbortSignal.timeout(50) });
console.log("selesai (tidak terjadi)");
} catch (error) {
console.log("tertangkap :", error.name, "-", error.message);
}
// --- 2. respons tiba sebelum batas waktu: berjalan normal ---
const response = await fetch(`${dasar}/`, { signal: AbortSignal.timeout(50) });
console.log("tepat waktu:", response.status, "-", await response.text());
server.closeAllConnections();
server.close();
});Output yang Dihasilkan:
tertangkap : TimeoutError - The operation was aborted due to timeout
tepat waktu: 200 - data langsung tersediaMari kita telaah baris-baris pentingnya:
- Baris 21 (
AbortSignal.timeout(50)): tidak ada controller yang ditulis — function statis ini mengembalikan sinyal yang otomatis terjadi pembatalan 50 ms setelah dibuat. Respons server yang baru tiba setelah 400 ms tentu ketinggalan. - Baris 24 (galat waktunya):
error.namebernilai"TimeoutError"dengan pesan"The operation was aborted due to timeout". Berbeda denganAbortError, nama ini memang khusus untuk kegagalan batas waktu. - Baris 28–29 (permintaan tepat waktu): endpoint kedua membalas seketika, sehingga sinyal 50 ms yang sama tidak sempat menyala dan permintaan berjalan seperti biasa. Batas waktu yang longgar tidak mengganggu permintaan yang sehat.
Membagikan Satu Sinyal ke Banyak Permintaan #
Sinyal AbortSignal tidak terikat pada satu fetch. Satu controller bisa melayani banyak permintaan sekaligus — dan satu panggilan abort() menghentikan semuanya serentak. Pola ini pas untuk layar yang dibuang pengguna: komponen dasbor memicu tiga permintaan (ringkasan, grafik, daftar) lewat satu sinyal; begitu pengguna berpindah halaman, controller.abort() dijalankan sekali dan ketiga permintaan berhenti bersamaan.
const controller = new AbortController();
const sinyal = { signal: controller.signal };
const [ringkasan, grafik] = await Promise.all([
fetch("/ringkasan", sinyal),
fetch("/grafik", sinyal),
]);
// pengguna berpindah halaman — satu panggilan untuk semuanya:
controller.abort();Tetapkan kebiasaan yang sama pada kolom pencarian: setiap ketikan baru membuat controller baru dan membatalkan controller sebelumnya, sehingga hanya permintaan terakhir yang berhak menampilkan hasilnya. Tanpa itu, balasan pertama bisa tiba terakhir dan menimpa hasil yang lebih baru — kejadian yang persis diceritakan di pembuka chapter ini.
Mengenali Jenis Galat dengan Andal #
Dua nama galat yang telah kita buktikan membedakan dua situasi yang berbeda pula, dan penanganannya sebaiknya dipisah:
error.name | Pemicu | Tindakan yang lazim |
|---|---|---|
AbortError | controller.abort() dari kode kita | Diamkan atau catat; jangan tampilkan sebagai galat |
TimeoutError | Batas waktu AbortSignal.timeout habis | Tawarkan coba lagi; kandidat untuk retry |
Pembanding yang aman adalah error.name — bukan isi error.message, karena pesan boleh berubah antar versi runtime. Untuk TimeoutError, percobaan ulang biasanya masuk akal; untuk AbortError buatan sendiri, pengulangan justru menentang keputusan yang barusan diambil kode.
Dengan pembagian peran ini, AbortController menjadi pengendali waktu di sisi client: pembatalan manual mengikuti niat pengguna, sementara AbortSignal.timeout menjaga server yang diam terlalu lama. Keduanya akan tampil lagi di chapter Retry dan Timeout Strategies sebagai bahan baku strategi ketahanan. Pertanyaan sejajar di sisi data — bagaimana mengambil ribuan data tanpa memboroskan memori — dijawab chapter Pagination.