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.

js
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:

text
tertangkap : AbortError - This operation was aborted
DOMException? true
signal.aborted: true
abort belakangan: 200 cepat | aborted: true

Mari kita telaah baris-baris pentingnya:

  1. 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.
  2. Baris 21–22 (controller dan jadwal abort): new AbortController() membuat pasangan controller–signal, dan setTimeout menjadwalkan abort() 10 ms kemudian — mensimulasikan pengguna yang berubah pikiran.
  3. Baris 25 ({ signal: controller.signal }): sinyal ditautkan ke permintaan. Dari sini nasib permintaan terikat pada keadaan sinyal.
  4. Baris 27–30 (penangkapan galat): pembatalan membuat fetch reject. Jenis galatnya selalu sama: error.name bernilai "AbortError" dengan pesan "This operation was aborted", dan error instanceof DOMException bernilai true. Pola pengecekan yang andal adalah memeriksa error.name === "AbortError" — bukan isi pesannya.
  5. Baris 30 (controller.signal.aborted): sinyal mencatat keadaannya sendiri; properti aborted bernilai true setelah abort() dipanggil, berguna untuk menandai permintaan mana yang dibatalkan.
  6. Baris 35–38 (abort yang terlambat): respons sudah dibaca habis lewat text() sebelum abort() dipanggil. Hasilnya damai: respons tetap 200, isinya utuh, dan pemanggilan abort() 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:

text
tertangkap : TimeoutError - The operation was aborted due to timeout
tepat waktu: 200 - data langsung tersedia

Mari kita telaah baris-baris pentingnya:

  1. 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.
  2. Baris 24 (galat waktunya): error.name bernilai "TimeoutError" dengan pesan "The operation was aborted due to timeout". Berbeda dengan AbortError, nama ini memang khusus untuk kegagalan batas waktu.
  3. 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.

js
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.namePemicuTindakan yang lazim
AbortErrorcontroller.abort() dari kode kitaDiamkan atau catat; jangan tampilkan sebagai galat
TimeoutErrorBatas waktu AbortSignal.timeout habisTawarkan 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.